qi-coding-agent 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. qi_agent/__init__.py +18 -0
  2. qi_agent/abort.py +40 -0
  3. qi_agent/auth.py +216 -0
  4. qi_agent/cli.py +2178 -0
  5. qi_agent/compaction.py +475 -0
  6. qi_agent/config.py +409 -0
  7. qi_agent/docs/cli.md +188 -0
  8. qi_agent/docs/compaction.md +148 -0
  9. qi_agent/docs/configuration.md +89 -0
  10. qi_agent/docs/containerization.md +80 -0
  11. qi_agent/docs/docs.json +1 -0
  12. qi_agent/docs/environment-variables.md +47 -0
  13. qi_agent/docs/extensions.md +382 -0
  14. qi_agent/docs/how-qi-works.md +93 -0
  15. qi_agent/docs/index.md +61 -0
  16. qi_agent/docs/json.md +112 -0
  17. qi_agent/docs/keybindings.md +83 -0
  18. qi_agent/docs/models.md +138 -0
  19. qi_agent/docs/packages.md +155 -0
  20. qi_agent/docs/providers.md +288 -0
  21. qi_agent/docs/quickstart.md +109 -0
  22. qi_agent/docs/security.md +69 -0
  23. qi_agent/docs/session-format.md +299 -0
  24. qi_agent/docs/sessions.md +127 -0
  25. qi_agent/docs/settings.md +138 -0
  26. qi_agent/docs/skills.md +124 -0
  27. qi_agent/docs/slash-commands.md +55 -0
  28. qi_agent/docs/themes.md +78 -0
  29. qi_agent/docs/tui.md +109 -0
  30. qi_agent/docs/usage.md +112 -0
  31. qi_agent/export_html.py +95 -0
  32. qi_agent/extensions.py +2267 -0
  33. qi_agent/llm.py +609 -0
  34. qi_agent/loader.py +299 -0
  35. qi_agent/model_catalog.py +108 -0
  36. qi_agent/models.py +103 -0
  37. qi_agent/packages.py +430 -0
  38. qi_agent/paths.py +242 -0
  39. qi_agent/pep723.py +74 -0
  40. qi_agent/presets.py +293 -0
  41. qi_agent/prompt.py +150 -0
  42. qi_agent/registry.py +485 -0
  43. qi_agent/runner.py +584 -0
  44. qi_agent/runtime.py +1940 -0
  45. qi_agent/session.py +739 -0
  46. qi_agent/settings.py +501 -0
  47. qi_agent/share.py +111 -0
  48. qi_agent/system_prompt.py +278 -0
  49. qi_agent/theme.py +389 -0
  50. qi_agent/themes/dark.json +90 -0
  51. qi_agent/themes/light.json +89 -0
  52. qi_agent/titling.py +90 -0
  53. qi_agent/tools/__init__.py +349 -0
  54. qi_agent/tools/shell.py +276 -0
  55. qi_agent/trust.py +106 -0
  56. qi_agent/tui.py +6224 -0
  57. qi_agent/workspaces.py +116 -0
  58. qi_coding_agent-0.1.0.dist-info/METADATA +151 -0
  59. qi_coding_agent-0.1.0.dist-info/RECORD +62 -0
  60. qi_coding_agent-0.1.0.dist-info/WHEEL +4 -0
  61. qi_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
  62. qi_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
qi_agent/docs/json.md ADDED
@@ -0,0 +1,112 @@
1
+ # JSON 事件流模式
2
+
3
+ `qi --mode json "问题"` 不走 TUI,而是把**每个事件**作为一个 JSON 对象打到 **stdout**,一行一个
4
+ (JSONL)。给脚本、管道、上游服务消费用。
5
+
6
+ ```bash
7
+ qi --mode json "总结这个仓库" | jq -r 'select(.kind=="text") | .text'
8
+ ```
9
+
10
+ - **`--mode json` 隐含无头**:即使不加 `-p` 也不进 TUI(判定见 `cli.py` 的 `interactive`)。
11
+ - **stdout 只有事件流**。启动提示、警告、`--verbose` 的进度都走 **stderr** —— 所以 `| jq` 不用
12
+ 过滤杂音。(`--verbose` 对这条流没有影响:JSON 模式本就输出全部事件。)
13
+ - **一个事件一行**。这是刻意的:多行美化会让消费端无法按行切分。契约由
14
+ `tests/test_cli_run.py::test_mode_json_emits_one_object_per_line` 钉住。
15
+
16
+ ## 1. 每个对象的形状
17
+
18
+ 固定 5 个键,顺序不保证,解析请按名取:
19
+
20
+ | 键 | 类型 | 说明 |
21
+ | --- | --- | --- |
22
+ | `kind` | string | 事件类型(见 §2) |
23
+ | `agent` | string \| null | 产出该事件的运行单元名(core 是固定名 `general`;子运行会用自己的名字) |
24
+ | `tool` | string \| null | 仅工具事件有值 |
25
+ | `text` | string | 该事件的主文本;没有则为 `""` |
26
+ | `data` | object | 结构化补充信息;没有则为 `{}` |
27
+
28
+ ```jsonc
29
+ {"kind": "agent_start", "agent": "general", "tool": null, "text": "", "data": {}}
30
+ {"kind": "text_delta", "agent": "general", "tool": null, "text": "问题在", "data": {}}
31
+ {"kind": "tool_start", "agent": "general", "tool": "read", "text": "", "data": {"args": {"path": "src/parse.py"}}}
32
+ {"kind": "tool_end", "agent": "general", "tool": "read", "text": "…文件内容…", "data": {"status": "ok", "duration_ms": 12, "exit_code": null, "error": null}}
33
+ {"kind": "text", "agent": "general", "tool": null, "text": "问题在偏移量计算。", "data": {}}
34
+ {"kind": "agent_end", "agent": "general", "tool": null, "text": "问题在偏移量计算。", "data": {"messages": [...], "usage": {"prompt_tokens": 3120, "completion_tokens": 140, "total_tokens": 3260, "llm_calls": 2, "turns": 1}}}
35
+ ```
36
+
37
+ ## 2. 事件类型
38
+
39
+ **core 今天会发的事件**(发射点在 `runner.py` / `runtime.py`):
40
+
41
+ | `kind` | 时机 | 关键 `data` 键 |
42
+ | --- | --- | --- |
43
+ | `agent_start` | 回合开始 | — |
44
+ | `text_delta` | 逐字增量(打字机效果) | — |
45
+ | `thinking_delta` | 思维链增量 | — |
46
+ | `assistant_message` | 每轮 LLM 回复完成一次 | `{"step": int, "tool_calls": [名字]}` |
47
+ | `tool_start` | 工具调用前 | `{"args": {…}}` |
48
+ | `tool_end` | 工具调用后 | `{"status": "ok"\|"error", "duration_ms": int, "exit_code": int\|null, "error": str\|null}` |
49
+ | `text` | 本回合**最终回答**(整段) | — |
50
+ | `agent_end` | 回合结束 | `{"messages": [...], "usage": {…}}` |
51
+ | `error` | 回合内的失败(模型拒答、工具异常等) | — |
52
+ | `compaction_start` / `compaction_end` | 自动压缩前后 | `{"auto": true, "tokens": int, …}` / `{"auto": true, "tokensBefore": int, …}` |
53
+
54
+ **声明了但 core 已不再发射**(`AgentEvent` 的注释里还留着,消费者仍会看到旧会话相关的处理代码):
55
+
56
+ | `kind` | 状态 |
57
+ | --- | --- |
58
+ | `dispatch` | v3 取消 auto 分派后不再产生。`cli.py` / `tui.py` / qi-web 仍**读取**它以便旧会话回放 |
59
+ | `opening` | 不再发射(旧会话里仍有,前端读取以正确回放) |
60
+
61
+ > **扩展之间的事件(`api.events`)不在这条流里。** 那是扩展对扩展的频道,与这里的宿主事件是
62
+ > 两套 API(见 [extensions.md](extensions.md) §5)。
63
+
64
+ ## 3. 两个必须知道的坑
65
+
66
+ ### `text_delta` 和 `text` 都会出现 —— 别把两者相加
67
+
68
+ `text_delta` 是逐字增量,`text` 是本回合的**完整回答**。同一条流里两者都有,所以:
69
+
70
+ - 想要打字机效果 → 只读 `text_delta`;
71
+ - 想要"这一轮的答案" → **只读 `text`**(CLI 与 TUI 就是这么做的);
72
+ - 两者混着累加会得到重复内容。
73
+
74
+ ### 顺序就是因果顺序,不要重排
75
+
76
+ 一轮里 `text_delta*` 与 `tool_start`/`tool_end` 按**真实发生顺序**穿插(工具调用之间的叙述不会被
77
+ 丢弃或后置)。把所有文本提到开头、把工具行推到末尾会失去因果 —— 那就等于把这条流降级成
78
+ "两堆东西"。
79
+
80
+ ## 4. 消费示例
81
+
82
+ ```bash
83
+ # 只要最终答案(忽略增量与工具)
84
+ qi --mode json "解释 src/parse.py" | jq -r 'select(.kind=="text") | .text'
85
+
86
+ # 跟踪工具调用(名字 + 状态 + 耗时)
87
+ qi --mode json "跑一下测试" | jq -r 'select(.kind=="tool_end") | "\(.tool) \(.data.status) \(.data.duration_ms)ms"'
88
+
89
+ # 取用量
90
+ qi --mode json "你好" | jq -c 'select(.kind=="agent_end") | .data.usage'
91
+ ```
92
+
93
+ ```python
94
+ # Python:一次一行读,不必缓冲(JSONL 的用处就在这)
95
+ import json, subprocess
96
+ proc = subprocess.Popen(["qi", "--mode", "json", "总结这个仓库"],
97
+ stdout=subprocess.PIPE, text=True)
98
+ for line in proc.stdout:
99
+ event = json.loads(line)
100
+ if event["kind"] == "text":
101
+ print(event["text"])
102
+ ```
103
+
104
+ ## 5. 稳定性
105
+
106
+ **可以依赖**:5 个键的名字 · §2 里"今天会发"的事件类型与它们的 `data` 键 · 一事件一行 · 因果顺序。
107
+
108
+ **不要依赖**:键的输出顺序 · 未知 `kind` 的存在与否(新事件会加进来)· `data` 里字段是否齐全
109
+ (不同 provider 给的信息不同)· 同一条流里"某类事件恰好出现几次"。
110
+
111
+ **读到不认识的 `kind` 时跳过即可,不要报错** —— 这与 qi 各处一致的原则相同(旧会话里也有已废弃
112
+ 的类型,新版本仍要能回放)。
@@ -0,0 +1,83 @@
1
+ # 键位
2
+
3
+ qi 的键位是**固定的**:没有 `keybindings.json` 这类配置文件。要加自己的键,写一个扩展用
4
+ `api.register_shortcut("ctrl+x", handler)`(key 用 Textual 的写法;坏键名只提示,不让启动失败)。
5
+
6
+ TUI 里 `/hotkeys` 会列出当前生效的键位,并如实标出哪些还没做。
7
+
8
+ ## 主键位
9
+
10
+ | 键 | 行为 |
11
+ | --- | --- |
12
+ | `escape` | 中断当前回合:**协作式**(未执行的工具补上"已中断"结果、半截回答照常落盘);再按一次、或 3s 宽限期到点仍未收尾 → 强制终止 |
13
+ | `escape` ×2 | 空编辑器连按两次(500ms 内):按 `settings.doubleEscapeAction` 开 `/tree` 或 `/fork`(默认 `tree`) |
14
+ | `ctrl+c` | 清空输入框;再按一次退出 |
15
+ | `ctrl+d` | 输入框为空时退出;非空删右侧字符 |
16
+ | `ctrl+o` | 展开 / 折叠工具输出(压缩块也用它展开看摘要) |
17
+ | `ctrl+t` | 显示 / 隐藏思考块 |
18
+ | `shift+tab` | 循环思考级别(off → minimal → … → max → off) |
19
+ | `ctrl+x` | 复制最后一条回答 |
20
+ | `ctrl+g` | 用 `$EDITOR` 编辑当前输入 |
21
+ | `ctrl+l` | 模型选择器(与 `/model` 同一个) |
22
+ | `ctrl+p` / `ctrl+shift+p` | 下一个 / 上一个模型(轮换清单由 `/scoped-models` 决定) |
23
+ | `ctrl+z` | 挂起(回到 shell) |
24
+
25
+ ## 输入层(多行编辑器)
26
+
27
+ | 键 | 行为 |
28
+ | --- | --- |
29
+ | `enter` | 提交 |
30
+ | `shift+enter` / `ctrl+j` | 换行 |
31
+ | `ctrl+b` / `ctrl+f` | 光标左 / 右 |
32
+ | `↑` / `↓` | 没有补全面板时翻输入历史(首次进入留住草稿、手动改动即退出);否则移动光标 |
33
+ | `alt+b` `alt+f` `alt+←/→` | 按词移动 |
34
+ | `alt+d` | 删后一个词 |
35
+ | `ctrl+-` | 撤销(`ctrl+z` 留给挂起) |
36
+ | `ctrl+w` / `ctrl+u` / `ctrl+k` / `alt+backspace` | 删词 / 删到行首 / 删到行尾 |
37
+ | `ctrl+y` / `alt+y` | kill-ring 的 yank / yank-pop(删掉的文本进环、连续删合并) |
38
+ | `ctrl+v` | 粘贴(含括号粘贴,多行可用) |
39
+
40
+ 编辑器最多 8 行(矮终端还会再降);长高的部分由 transcript 让位。
41
+
42
+ ## 补全面板
43
+
44
+ | 键 | 行为 |
45
+ | --- | --- |
46
+ | 行首 `/xx` | 命令名补全 |
47
+ | `/model` `/thinking` `/login` | 参数补全(模型 `provider/model` / 思考级别 / provider) |
48
+ | `@xx`(任意位置) | 路径补全:装了 `fd` 走全树搜索(尊重 `.gitignore`),否则只扫当前目录一层;目录优先 |
49
+ | `tab` | 接受候选(**无候选时仍是缩进**) |
50
+ | `↑` / `↓` | 面板开着时选候选;否则翻输入历史 / 移动光标 |
51
+ | `escape` | 先关面板,再考虑中断 |
52
+
53
+ ## 会话选择器(`/resume`)
54
+
55
+ | 键 | 行为 |
56
+ | --- | --- |
57
+ | 输入框打字 | 过滤:空格分词(模糊子序列)/ `"短语"` 精确连续 / `re:<正则>` 正则 |
58
+ | `tab` | 切换范围:**当前目录**(默认)↔ 全部 |
59
+ | `↑` / `↓` / `enter` | 选 / 恢复 |
60
+ | `ctrl+n` | 只看命名会话 |
61
+ | `ctrl+s` | 排序循环:树状 → 最近 → 最相关 |
62
+ | `ctrl+p` | 显示 / 隐藏会话文件路径 |
63
+ | `ctrl+r` | 重命名选中会话 |
64
+ | `ctrl+d` | 删除选中会话(**先确认**;当前会话删不掉) |
65
+ | `escape` | 先退出重命名态 / 取消删除确认,再关面板 |
66
+
67
+ ## 树选择器(`/tree`)
68
+
69
+ | 键 | 行为 |
70
+ | --- | --- |
71
+ | 输入框打字 | 搜索节点(空格分词,全部命中才显示) |
72
+ | `↑` / `↓` / `enter` | 选 / 跳到该节点继续 |
73
+ | `ctrl+d` / `ctrl+t` / `ctrl+u` / `ctrl+l` / `ctrl+a` | 过滤:默认(隐藏状态类)/ 隐藏工具结果 / 只看用户消息 / 只看有标签 / 全部 |
74
+ | `ctrl+o` / `shift+ctrl+o` | 过滤模式循环 / 反向循环 |
75
+ | `shift+l` | 编辑标签(`enter` 保存;留空 = 清除) |
76
+ | `shift+t` | 标签是否附带时间戳 |
77
+ | `escape` | 先退出标签编辑,再关面板 |
78
+
79
+ ## 尚未支持
80
+
81
+ | 键 | 期望用途 | 现状 |
82
+ | --- | --- | --- |
83
+ | `ctrl+v` | 粘贴图片 | 无图片输入,目前只会粘文本 |
@@ -0,0 +1,138 @@
1
+ # 选一个模型
2
+
3
+ qi 通过 litellm 调各家模型。provider 与模型定义都在 `models.json`;**默认模型**在 `settings.json`。
4
+ 凭证明文只进 `auth.json` 或环境变量,`models.json` 只允许**引用**。
5
+
6
+ ## 选一条连接
7
+
8
+ | 想怎么接 | 用什么 |
9
+ | --- | --- |
10
+ | 用 qi 自带的**预置**(国产为主:baseUrl + 约定环境变量 + 核过的模型清单) | `qi init --preset <名>` 或交互式 `qi init` |
11
+ | 自己写一个 provider(自建代理 / 聚合平台) | 编辑 `models.json` 的 `providers` 段,或 `qi init -y` |
12
+ | 本地无鉴权端点(Ollama / llama.cpp / LM Studio) | 写 provider + 一个字面 `apiKey`(如 `"none"`) |
13
+ | 按次换模型,不写配置 | `qi --provider <名> --model <provider/模型>` |
14
+
15
+ ## 认证
16
+
17
+ 凭证解析顺序(auth store → 约定环境变量 → `models.json` 的 `apiKey` 引用)、约定变量表与排查口径见
18
+ [providers.md](providers.md)。`qi auth login <provider>` 写 `auth.json`(0600);TUI 里是 `/login`。
19
+
20
+ ## 选默认模型
21
+
22
+ ```jsonc
23
+ // ~/.qi/agent/settings.json
24
+ { "defaultProvider": "deepseek", "defaultModel": "deepseek-chat" }
25
+ ```
26
+
27
+ - **唯一来源是 `settings.json`**:`models.json` 里的 `defaultProvider` / `defaultModel`
28
+ **完全不参与**(不是优先级更低),写了会被忽略并在 `qi doctor` 里报出来。
29
+ - 缺任一项就启动失败,并给出可照抄的迁移命令。
30
+ - 查看:`qi --list-models [搜索词]`(缺密钥的也列,这是诊断面;解析不出凭证的 provider 不列)。
31
+
32
+ ## 接本地模型
33
+
34
+ 本地端点不需要密钥,但要让它**进 `/model` 与 `ctrl+p` 清单**:
35
+
36
+ ```jsonc
37
+ { "providers": { "local": {
38
+ "baseUrl": "http://localhost:1234/v1",
39
+ "api": "openai-completions",
40
+ "apiKey": "none", // 字面值 —— resolve_key 认它,请求只是多一个 Authorization 头
41
+ "models": [{ "id": "qwen3-8b", "contextWindow": 32768, "maxTokens": 4096 }]
42
+ } } }
43
+ ```
44
+
45
+ `ollama` 是唯一**免密钥**的内置特例(显示为"无需密钥(本地 provider)")。
46
+
47
+ ## 配一个兼容端点
48
+
49
+ ```jsonc
50
+ {
51
+ "providers": {
52
+ "deepseek": {
53
+ "baseUrl": "https://api.deepseek.com/v1",
54
+ "api": "openai-completions",
55
+ "apiKey": "$DEEPSEEK_API_KEY",
56
+ "headers": {},
57
+ "models": [
58
+ { "id": "deepseek-chat", "name": "DeepSeek Chat",
59
+ "reasoning": false, "input": ["text"],
60
+ "contextWindow": 128000, "maxTokens": 16384 }
61
+ ]
62
+ }
63
+ }
64
+ }
65
+ ```
66
+
67
+ | 字段 | 必填 | 默认 | 说明 |
68
+ | --- | --- | --- | --- |
69
+ | `providers.<name>.baseUrl` | | | API endpoint |
70
+ | `providers.<name>.api` | | `openai-completions` | `openai-completions` / `openai-responses` / `anthropic-messages` / `google-generative-ai` |
71
+ | `providers.<name>.apiKey` | | | 引用:字面量 / `$ENV` / `${ENV}` / `!command`;省略则查 auth store / 约定环境变量 |
72
+ | `providers.<name>.headers` | | | 自定义请求头,值语法同 `apiKey` |
73
+ | `providers.<name>.models[]` | | | 模型列表,每个至少 `id` |
74
+ | `models[].reasoning` | | `false` | 是否支持扩展思考(决定要不要下发 `reasoning_effort`) |
75
+ | `models[].contextWindow` | | `128000` | 上下文窗口 token(自动压缩的阈值基于它) |
76
+ | `models[].maxTokens` | | `16384` | 最大输出 token(作为请求的 `max_tokens` 上限) |
77
+ | `models[].input` | | `["text"]` | 输入类型 |
78
+
79
+ 模型条目可缺省,`resolve_model()` 会用 provider 默认 + 内置默认补齐。
80
+
81
+ **`apiKey` 值语法**:
82
+
83
+ | 写法 | 行为 |
84
+ | --- | --- |
85
+ | `"sk-abc"` | 字面量 |
86
+ | `"$ENV"` / `"${ENV}"` | 环境变量插值;**缺失 = 整个值解析失败**(不返回半截) |
87
+ | `"!command args"` | 执行命令取 stdout;非零退出 / 超时(30s)/ 引号不配对 = 失败。**不走 shell**(`shlex` 拆参 + `shell=False`) |
88
+ | `"$$x"` / `"$!x"` | 转义:得到字面 `$x` / `!x` |
89
+
90
+ ## 手写还是让 `qi init` 写
91
+
92
+ `qi init` 引导默认模型,三步:**选 provider**(已有 / 预置 `[预置]` / 新建 → Base URL → API 类型 →
93
+ API Key,直接写 `auth.json`)→ **Add Models**(`+ Add a model` / `↻ Refresh model list (GET /models)` /
94
+ `✓ Done` 循环)→ **Activate LLM Model**(写 `defaultProvider` / `defaultModel` 进 `settings.json`)。
95
+ `-l` 写项目 `<项目>/.qi/models.json`(否则全局)。
96
+
97
+ ```bash
98
+ qi init # 交互式
99
+ qi init --list-presets # 看内置预置 provider
100
+ qi init --preset deepseek # 物化预置(写 models.json)+ 设为默认;可逗号写多个;幂等,不覆盖你写过的值
101
+ qi init --refresh deepseek # 拉厂商 /models 对齐本地清单(只加不删)
102
+
103
+ qi init -y --provider deepseek --model deepseek-chat \
104
+ --base-url https://api.deepseek.com/v1 --api openai-completions --api-key sk-xxx
105
+ ```
106
+
107
+ | 选项 | 说明 |
108
+ | --- | --- |
109
+ | `-y, --yes` | 非交互:必须配 `--provider`(与 `--model`) |
110
+ | `--provider <name>` / `--model <id>` | provider 名(已有或新建)/ 模型 id |
111
+ | `--base-url <url>` / `--api <type>` | endpoint / API 形态 |
112
+ | `--api-key <key>` / `--api-key-env <VAR>` | 写 `auth.json` / 在 `models.json` 里引用环境变量 |
113
+ | `--reasoning` / `--no-reasoning` | 模型是否支持扩展思考 |
114
+ | `--context-window <int>` / `--max-tokens <int>` | 两个元数据(默认 128000 / 16384) |
115
+ | `-l, --local` | 写项目 `.qi/models.json` |
116
+
117
+ 已有模型按 id upsert,重跑安全;`--preset` / `--refresh` 都**只补缺不覆盖**。
118
+
119
+ ## 查找层级
120
+
121
+ ```text
122
+ models.json: $QI_AGENT_CONFIG(env 指定文件,最高)
123
+ → <项目>/.qi/models.json
124
+ → ~/.qi/agent/models.json
125
+
126
+ 默认模型: <项目>/.qi/settings.json > ~/.qi/agent/settings.json(唯一来源)
127
+ ```
128
+
129
+ 键级深合并,内层覆盖外层;文件不存在则跳过。provider 按名覆盖,`models` 数组**整体替换**。
130
+
131
+ ## 排错
132
+
133
+ | 现象 | 先看 |
134
+ | --- | --- |
135
+ | 模型不在 `/model` 或 `ctrl+p` 清单里 | 清单只列**解析得出凭证**的 provider —— 跑 `qi --list-models` 看 `credential` 列 |
136
+ | "我明明设了环境变量" | `qi doctor` 会给出凭证**来源**(`auth(...)` / `env:XXX` / `config:...`),区分"没读到"与"读到了但为空" |
137
+ | provider 拒收 `reasoning_effort` | qi 会自动去掉参数重试并在 footer 提示一次;也可把该模型的 `reasoning` 改成 `false` |
138
+ | 相容端点返回 4xx | 先确认 `api` 形态选对(`openai-completions` vs `openai-responses` vs `anthropic-messages`) |
@@ -0,0 +1,155 @@
1
+ # 扩展的安装与声明
2
+
3
+ > 装扩展 = 调 pip(或放一个目录);qi **不做**这件事,只做「**声明层 + 比对**」。
4
+ > 相关:[cli.md](cli.md)(命令速查)、[extensions.md](extensions.md)(扩展机制与 API)、[settings.md](settings.md)(`packages` / `extensions` 字段)。
5
+
6
+ ## 1. 谁负责跱这一步
7
+
8
+ `qi install` **存在**,但它只是**帮你跱一步**:调一次 pip,然后把声明写进
9
+ `settings.packages`。**它不替代“想清楚装到哪个环境”这件事** —— 三种目标环境的行为真的不同:
10
+
11
+ 1. **目标环境有三种**:qi 自己的 venv、`uv tool` 托管的环境、只读的系统解释器(Homebrew / 系统 Python)。
12
+ qi 每次都把要跑的命令**先打出来**,所以你能看出装到了哪里;只读解释器下 pip 会自己报错,
13
+ qi 把输出原样给你并补上 `uv tool install qi-coding-agent --with <包>` 那条出路。
14
+ 2. **`uv tool` 会整个重建环境**。uv 文档的原话:tool 环境 "may be upgraded via `uv tool upgrade`,
15
+ or **re-created entirely** via subsequent `uv tool install`"。所以 pip 装进去的扩展**会被抹掉**
16
+ —— 声明还在,`qi doctor` 会把它报出来(这正是“声明层”存在的意义)。
17
+ 3. **`remove` 不卸包**:它只从声明里移除,并把 `pip uninstall` 命令给你。
18
+ 装/卸包是 pip 的事,声明是你的事。
19
+ 4. **声明与实装始终是两件事**:声明说"这个环境该有什么",实装由 pip 决定。所以 `qi list` /
20
+ `qi doctor` 永远做**两个方向**的比对 —— 即使你用的是 `qi install`。
21
+
22
+ ## 2. 声明怎么写
23
+
24
+ `settings.json` 的 `packages` 是字符串数组(也容忍 `{"name": …}` 这种结构体写法):
25
+
26
+ ```json
27
+ { "packages": ["pip:qi-mcp>=0.2", "local:./extensions/qi-agents"] }
28
+ ```
29
+
30
+ | 写法 | 通道 | 名字从哪来 |
31
+ | --- | --- | --- |
32
+ | `pip:qi-mcp` | pip | 包名 |
33
+ | `qi-mcp` | pip | 同上(前缀可省) |
34
+ | `qi-mcp>=0.2` | pip | 名字 + 版本约束(整个 spec 会进装法) |
35
+ | `Qi.MCP` | pip | **PEP 503 归一** → `qi-mcp`(与「已装」侧同一规则,否则一边算命中一边不算) |
36
+ | `qi-mcp @ git+https://host/repo` | pip | **显式名字**(PEP 508 的 `名字 @ URL`) |
37
+ | `local:./extensions/qi-mcp` | 目录 | 目录名 |
38
+ | `/abs/path`、`./rel`、`~/x`、`file:…` | 目录 | `Path(...).name` |
39
+ | `{"name": "pip:qi-mcp"}` | 按内容判 | 取 `name` / `spec` / `source` 里第一个非空的 |
40
+
41
+ > **裸 URL 故意不猜名字**:把 `git+https://host/qi-mcp` 读成一个叫 `git` 的包,比认不出更坏。
42
+ > 这种声明会被**原样回显**并提示改成 `名字 @ URL` —— 不静默丢。
43
+
44
+ 作用域:用户级 `~/.qi/agent/settings.json` 与项目级 `<git 根>/.qi/settings.json` **都读**,
45
+ 同名以**项目**为准(近者胜)。两份**分别解析**而不是合并数组 —— 合并会让一侧的声明落到另一侧的基准上。
46
+
47
+ ## 3. 已装扩展从哪来
48
+
49
+ 三个通道,与**装载路径**是同一套(所以报告不会与"实际能加载什么"错位):
50
+
51
+ | 通道 | 位置 |
52
+ | --- | --- |
53
+ | pip | entry point 组 `qi.extensions`(`importlib.metadata`) |
54
+ | 目录 | `~/.qi/agent/extensions/<名>/extension.py`、项目 `.qi/extensions/<名>/extension.py` |
55
+ | 附加 | `qi -e <dir>`、`settings.extensions[]` |
56
+
57
+ > **未信任的项目目录不扫** —— 与装载路径同一条门控。否则未信任项目的 `.qi/extensions/`
58
+ > 会让报告显得"已装",而实际一个都没加载。
59
+
60
+ 同名去重:entry point 先到先得。
61
+
62
+ ## 4. 两条装法,失效面不同
63
+
64
+ `qi doctor` / `qi list` 会把两条都打出来,**不替你选**:
65
+
66
+ ```bash
67
+ # ① 直接装进 qi 所在的那个解释器(最直接)
68
+ /path/to/venv/bin/python -m pip install "qi-mcp>=0.2"
69
+
70
+ # ② 写进 uv tool 的托管依赖(uv tool 重建 / 升级后仍然在 —— 推荐)
71
+ uv tool install qi-coding-agent --with "qi-mcp>=0.2"
72
+ ```
73
+
74
+ | | ① 直接 pip | ② uv 托管依赖 |
75
+ | --- | --- | --- |
76
+ | 装到哪 | `sys.executable` 对应的环境 | uv 的 tool 环境 |
77
+ | uv tool 重建后 | **丢**(声明还在,所以 `qi doctor` 会把它报出来) | 保留 |
78
+ | 只读解释器 | 失败(pip 自己报错) | 可行 |
79
+ | 适合 | 自己管 venv 的人 | 用 `uv tool` 装 qi 的人 |
80
+
81
+ ## 5. 怎么读输出
82
+
83
+ 装好了但没声明(这份环境里三个扩展都是 editable 装进来的):
84
+
85
+ ```text
86
+ $ qi list
87
+ 扩展
88
+ ┏━━━━━━━━━━━┳━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━┓
89
+ ┃ 扩展 ┃ 通道 ┃ 版本 ┃ 来源 ┃ 声明 ┃
90
+ ┡━━━━━━━━━━━╇━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━┩
91
+ │ qi-mcp │ pip │ 0.1.0 │ qi-mcp 0.1.0 · user │ 否 │
92
+ │ qi-agents │ pip │ 0.1.0 │ qi-agents 0.1.0 · user │ 否 │
93
+ │ qi-web │ pip │ 0.1.0 │ qi-web 0.1.0 · user │ 否 │
94
+ └───────────┴──────┴───────┴────────────────────────┴──────┘
95
+ 已装但未声明(3): qi-mcp、qi-agents、qi-web —— 写进 settings.packages 才能在 uv
96
+ tool 重建后补回
97
+ ```
98
+
99
+ 声明里有一条装不上、还有一条认不出时(`packages: ["pip:qi-todo", "qi-agents", "git+https://host/repo"]`):
100
+
101
+ ```text
102
+ 扩展
103
+ ┏━━━━━━━━━━━┳━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┓
104
+ ┃ 扩展 ┃ 通道 ┃ 版本 ┃ 来源 ┃ 声明 ┃
105
+ ┡━━━━━━━━━━━╇━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━┩
106
+ │ qi-mcp │ pip │ 0.1.0 │ qi-mcp 0.1.0 · user │ 否 │
107
+ │ qi-agents │ pip │ 0.1.0 │ qi-agents 0.1.0 · user │ 是 │
108
+ │ qi-web │ pip │ 0.1.0 │ qi-web 0.1.0 · user │ 否 │
109
+ │ qi-todo │ pip │ — │ pip:qi-todo · settings:user │ 未安装 │
110
+ └───────────┴──────┴───────┴─────────────────────────────┴────────┘
111
+ 包声明:
112
+ ✗ 声明了但没装: pip:qi-todo (settings:user)
113
+ /…/bin/python3 -m pip install "qi-todo"
114
+ uv tool install qi-coding-agent --with "qi-todo"
115
+ ⚠ 无法解析的声明: git+https://host/repo
116
+ 用 `名字 @ URL` 写法才认得出名(如 qi-mcp @ git+https://host/repo)
117
+ 已装但未声明(2): qi-mcp、qi-web —— 写进 settings.packages 才能在 uv tool 重建后补回
118
+ ```
119
+
120
+ 四类信息各自的含义:
121
+
122
+ | 输出 | 含义 | 该怎么办 |
123
+ | --- | --- | --- |
124
+ | 表格里的普通行 | 实际能加载的扩展(通道 / 版本 / 来源 / 是否声明) | — |
125
+ | `未安装` 的行 + `✗ 声明了但没装` | 声明在、加载不到 —— 最可能是 `uv tool` 重建把它抹了 | 复制下面给出的命令 |
126
+ | `⚠ 无法解析的声明` | 这条声明 qi 不认识(典型:裸 URL) | 改成 `名字 @ URL` |
127
+ | `已装但未声明` | 装了,但 `settings.packages` 里没有 | 写上 —— 否则下次重建后没人知道它该在 |
128
+
129
+ `qi doctor` 的包声明节与 `qi list` 用**同一个**打印函数,所以两边一致;doctor 还会顺带查配置、
130
+ 凭证与扩展的宿主依赖契约(见 §7)。
131
+
132
+ ## 6. 报告的数据结构
133
+
134
+ 嵌入方拿到的是 `packages.PackageReport`:
135
+
136
+ | 字段 | 含义 |
137
+ | --- | --- |
138
+ | `installed` | 实际可加载的扩展(三个通道,去重后) |
139
+ | `declared` | 可解析的声明(项目覆盖用户) |
140
+ | `missing` | 声明了但没装 —— **每一个都带可复制的装法** |
141
+ | `undeclared` | 装了但没声明 |
142
+ | `unparsed` | 认不出的声明(保留**原始 spec**,回显给用户) |
143
+ | `consistent` | 上面三个偏差列表都空 |
144
+
145
+ 两个方向**不合成一个数**:`missing` 与 `undeclared` 的处置完全不同(一个是"装回来",
146
+ 一个是"补声明"),合成一个"不一致"会让人无从下手。
147
+
148
+ ## 7. 边界
149
+
150
+ - 这是**声明与实装的比对**,不是依赖求解器:qi 不去读每个扩展的 `requires()` 算冲突树。
151
+ 那件事在**装载时**按「扩展有没有把宿主写进依赖」单独报(见 [extensions.md §10](extensions.md))。
152
+ - **目录通道的 PEP 723 声明**:`# /// script` 里的 `dependencies` 目前只用于上面那条宿主依赖检查;
153
+ `qi doctor` 还**不**据它去查"这些依赖装齐了没有"。
154
+ - `settings.packages`(包声明)与 `settings.extensions`(资源路径)是**两件事**:
155
+ 前者是"这个环境该装什么",后者是"到哪里去找扩展目录"。