init: MindOS CLI 本地执行体(从 mindOSv2/mindos-cli 独立)

- 独立 pyproject.toml(pip install -e .)
- vendor_hermes.sh 已改为显式路径模式(不再依赖相对目录)
- 包含 hermes vendor 快照
This commit is contained in:
2026-04-28 13:12:54 +08:00
commit 69dd868e2f
171 changed files with 128885 additions and 0 deletions
+254
View File
@@ -0,0 +1,254 @@
"""
IMA 连接器工具 (ima_tool.py)
与腾讯会议不同,IMA 是纯 REST APIHTTP POST + JSON),
无需外部脚本,直接在进程内调用。
┌──────────────────────────────────────────────────────┐
│ 绑定流程:侧边栏 inline UI(填写 Client ID + API Key)│
│ 调用流程:SKILL.md + Agent 对话 │
└──────────────────────────────────────────────────────┘
凭证存储(per-user JSON):
/opt/apps/mindos-next/backend/data/ima_tokens.json
格式:{ "userId": {"client_id": "...", "api_key": "..."} }
IMA OpenAPI 文档:https://ima.qq.com/agent-interface
"""
import json
import logging
import os
import urllib.error
import urllib.request
from typing import Any, Dict
from tools.registry import registry, tool_error, tool_result
logger = logging.getLogger(__name__)
# ── 常量 ──
_BASE_URL = "https://ima.qq.com/"
CREDENTIALS_URL = "https://ima.qq.com/agent-interface"
_SKILL_VERSION = "1.1.3"
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 凭证存取(per-userwiki/{userId}/.config/ima.json
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
from tools._user_config import user_config_read, user_config_write, user_config_delete
def get_user_credentials(user_id: str) -> Dict[str, str]:
"""返回 {"client_id": "...", "api_key": "..."} 或空 dict"""
creds = user_config_read(user_id, "ima")
if creds.get("client_id") and creds.get("api_key"):
return creds
# 兜底:全局 env
cid = os.environ.get("IMA_OPENAPI_CLIENTID", "").strip()
key = os.environ.get("IMA_OPENAPI_APIKEY", "").strip()
if cid and key:
return {"client_id": cid, "api_key": key}
return {}
def set_user_credentials(user_id: str, client_id: str, api_key: str) -> None:
"""保存或清除指定用户的 IMA 凭证。两者为空则删除条目。"""
client_id = client_id.strip()
api_key = api_key.strip()
if client_id and api_key:
user_config_write(user_id, "ima", {"client_id": client_id, "api_key": api_key})
else:
user_config_delete(user_id, "ima")
def has_user_credentials(user_id: str) -> bool:
return bool(get_user_credentials(user_id))
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# HTTP 调用层(进程内,无需子进程)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
def _ima_call(api_path: str, body: dict, creds: Dict[str, str]) -> Dict[str, Any]:
"""
直接发送 HTTP POST 到 IMA OpenAPI。
返回解析后的 JSON dict,出错时返回 {"error": ...}。
"""
url = _BASE_URL + api_path.lstrip("/")
headers = {
"Content-Type": "application/json",
"ima-openapi-clientid": creds["client_id"],
"ima-openapi-apikey": creds["api_key"],
"ima-openapi-ctx": f"skill_version={_SKILL_VERSION}",
}
data = json.dumps(body).encode("utf-8")
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
try:
with urllib.request.urlopen(req, timeout=30) as r:
return json.loads(r.read().decode("utf-8"))
except urllib.error.HTTPError as e:
body_text = e.read().decode("utf-8", errors="replace")[:500]
return {"error": f"HTTP {e.code}", "detail": body_text}
except urllib.error.URLError as e:
return {"error": f"网络错误: {e}"}
except Exception as e:
return {"error": str(e)}
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ① ima_list_profiles — 连接状态检查
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
def _ima_list_profiles_handler(args: dict, **kwargs) -> str:
user_id = str(args.get("user_id", "")).strip()
if not user_id:
return tool_error("user_id 参数必填")
if not has_user_credentials(user_id):
return tool_result(
success=True,
profiles=[],
count=0,
credentialsConfigured=False,
credentialsUrl=CREDENTIALS_URL,
notice="请前往上述链接获取 Client ID 和 API Key,并在侧边栏绑定。",
)
creds = get_user_credentials(user_id)
# 用 list_note (limit=1) 验证连通性
logger.info("[ImaTool] checking connectivity for user=%s", user_id[:8])
result = _ima_call("openapi/note/v1/list_note", {"cursor": "", "limit": 1}, creds)
if "error" in result:
status = "error"
message = f"连接异常:{result['error']}"
else:
status = "connected"
message = "IMA 已连接。"
profiles = [{
"profileName": f"{user_id[:12]}_ima",
"label": "IMA 笔记 & 知识库",
"status": status,
"credentialsConfigured": True,
}]
return tool_result(
success=True,
profiles=profiles,
count=len(profiles),
credentialsConfigured=True,
message=message,
)
registry.register(
name="ima_list_profiles",
toolset="connectors",
description="检查 IMA 连接状态,返回账号信息。",
emoji="📝",
handler=_ima_list_profiles_handler,
schema={
"name": "ima_list_profiles",
"description": "检查 IMA (QQ 笔记/知识库) 是否已连接,返回连接状态。",
"parameters": {
"type": "object",
"properties": {
"user_id": {"type": "string", "description": "MINDOS_USER_ID"},
},
"required": ["user_id"],
},
},
)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ② ima_query — 笔记和知识库操作
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
_ALLOWED_API_PATHS = {
# Notes
"openapi/note/v1/list_notebook",
"openapi/note/v1/list_note",
"openapi/note/v1/search_note_book",
"openapi/note/v1/get_doc_content",
"openapi/note/v1/import_doc",
"openapi/note/v1/append_doc",
# Knowledge Base
"openapi/knowledge/v1/list_knowledge_base",
"openapi/knowledge/v1/search_knowledge_base",
"openapi/knowledge/v1/create_knowledge_base",
"openapi/knowledge/v1/delete_knowledge_base",
"openapi/knowledge/v1/add_url",
}
def _ima_query_handler(args: dict, **kwargs) -> str:
user_id = str(args.get("user_id", "")).strip()
api_path = str(args.get("api_path", "")).strip()
if not user_id:
return tool_error("user_id 参数必填")
if not api_path:
return tool_error("api_path 参数必填")
if api_path not in _ALLOWED_API_PATHS:
return tool_error(
f"不支持的 API 路径:{api_path}"
f"可选:{', '.join(sorted(_ALLOWED_API_PATHS))}"
)
if not has_user_credentials(user_id):
return tool_error(
f"IMA 凭证未配置。请前往 {CREDENTIALS_URL} 获取 Client ID 和 API Key"
"并在侧边栏「IMA」处绑定账号。"
)
body = args.get("body", {})
if not isinstance(body, dict):
return tool_error("body 必须是 JSON 对象")
creds = get_user_credentials(user_id)
logger.info("[ImaTool] call %s (user=%s)", api_path, user_id[:8])
result = _ima_call(api_path, body, creds)
if "error" in result:
return tool_error(f"调用失败:{result['error']}")
return tool_result(success=True, data=result)
registry.register(
name="ima_query",
toolset="connectors",
description="查询或操作 IMA 笔记和知识库。",
emoji="📝",
handler=_ima_query_handler,
schema={
"name": "ima_query",
"description": (
"调用 IMA OpenAPI 操作笔记或知识库。需先在侧边栏完成凭证绑定。\n"
"笔记接口示例:\n"
" 搜索笔记: api_path=openapi/note/v1/search_note_book, body={\"search_type\":0,\"query_info\":{\"title\":\"xx\"},\"start\":0,\"end\":10}\n"
" 读取正文: api_path=openapi/note/v1/get_doc_content, body={\"doc_id\":\"xxx\",\"target_content_format\":0}\n"
" 新建笔记: api_path=openapi/note/v1/import_doc, body={\"content\":\"## 标题\\n内容\",\"content_format\":1}\n"
" 列出笔记: api_path=openapi/note/v1/list_note, body={\"cursor\":\"\",\"limit\":20}\n"
),
"parameters": {
"type": "object",
"properties": {
"user_id": {"type": "string", "description": "MINDOS_USER_ID"},
"api_path": {
"type": "string",
"description": "IMA API 路径",
"enum": sorted(_ALLOWED_API_PATHS),
},
"body": {
"type": "object",
"description": "API 请求体(JSON 对象)",
},
},
"required": ["user_id", "api_path"],
},
},
)