@armadra/agent 0.2.1 → 0.3.0

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 (80) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +55 -11
  3. package/dist/agent/session-cache.js +5 -2
  4. package/dist/agent/session-state.js +2 -1
  5. package/dist/agent/session-sync.js +9 -2
  6. package/dist/agent/session.js +8 -7
  7. package/dist/ai/apis/openai-responses.js +2 -1
  8. package/dist/ai/cache/fingerprint.d.ts +1 -1
  9. package/dist/ai/cache/fingerprint.js +1 -1
  10. package/dist/ai/cache/reporting.js +2 -1
  11. package/dist/ai/providers/channels.d.ts +51 -0
  12. package/dist/ai/providers/channels.js +96 -0
  13. package/dist/ai/providers/enrich.d.ts +34 -0
  14. package/dist/ai/providers/enrich.js +86 -0
  15. package/dist/ai/providers/models-dev-cache.d.ts +53 -0
  16. package/dist/ai/providers/models-dev-cache.js +147 -0
  17. package/dist/ai/providers/models-dev.d.ts +99 -0
  18. package/dist/ai/providers/models-dev.js +315 -0
  19. package/dist/ai/providers/registry.d.ts +35 -4
  20. package/dist/ai/providers/registry.js +177 -24
  21. package/dist/ai/types.d.ts +21 -1
  22. package/dist/bundle/ama.cjs +4075 -2008
  23. package/dist/cli/args.d.ts +6 -3
  24. package/dist/cli/args.js +30 -3
  25. package/dist/cli/bootstrap.js +1 -0
  26. package/dist/cli/compose-providers.js +3 -0
  27. package/dist/cli/compose-session.d.ts +3 -0
  28. package/dist/cli/compose.d.ts +3 -1
  29. package/dist/cli/compose.js +24 -9
  30. package/dist/cli/deps.d.ts +2 -0
  31. package/dist/cli/main.d.ts +2 -1
  32. package/dist/cli/main.js +13 -1
  33. package/dist/cli/startup-steps.js +6 -1
  34. package/dist/cli/subcommands/config.d.ts +11 -2
  35. package/dist/cli/subcommands/config.js +80 -4
  36. package/dist/cli/subcommands/context.js +1 -0
  37. package/dist/cli/subcommands/init.d.ts +6 -0
  38. package/dist/cli/subcommands/init.js +22 -0
  39. package/dist/cli/subcommands/model-meta.d.ts +16 -0
  40. package/dist/cli/subcommands/model-meta.js +54 -0
  41. package/dist/cli/subcommands/models-cache-probe.js +1 -1
  42. package/dist/cli/subcommands/models-discover.d.ts +6 -0
  43. package/dist/cli/subcommands/models-discover.js +37 -9
  44. package/dist/cli/subcommands/models.js +26 -18
  45. package/dist/cli/subcommands/providers-list.d.ts +8 -0
  46. package/dist/cli/subcommands/providers-list.js +97 -0
  47. package/dist/cli/subcommands/providers-plan.d.ts +79 -0
  48. package/dist/cli/subcommands/providers-plan.js +215 -0
  49. package/dist/cli/subcommands/providers.d.ts +27 -0
  50. package/dist/cli/subcommands/providers.js +433 -0
  51. package/dist/config/init.d.ts +30 -0
  52. package/dist/config/init.js +92 -0
  53. package/dist/config/json-schema.d.ts +14 -0
  54. package/dist/config/json-schema.js +182 -0
  55. package/dist/config/schema.d.ts +1 -1
  56. package/dist/config/schema.js +74 -11
  57. package/dist/config/types.d.ts +23 -5
  58. package/dist/config/types.js +2 -0
  59. package/dist/modes/image-input.d.ts +27 -0
  60. package/dist/modes/image-input.js +78 -0
  61. package/dist/modes/interactive/commands.js +2 -3
  62. package/dist/modes/interactive/interactive-mode.js +5 -1
  63. package/dist/modes/interactive/line/line-mode.js +3 -1
  64. package/dist/modes/interactive/startup-ui.d.ts +7 -2
  65. package/dist/modes/interactive/startup-ui.js +41 -11
  66. package/dist/modes/print/print-mode.d.ts +2 -0
  67. package/dist/modes/print/print-mode.js +12 -1
  68. package/dist/modes/rpc/commands.js +7 -2
  69. package/dist/rpc.d.ts +2 -0
  70. package/dist/session/projection.js +5 -1
  71. package/dist/session/types.d.ts +2 -0
  72. package/dist/tools/image-file.d.ts +31 -0
  73. package/dist/tools/image-file.js +114 -0
  74. package/dist/tools/read.d.ts +4 -8
  75. package/dist/tools/read.js +15 -64
  76. package/docs/providers.md +216 -1
  77. package/docs/rpc.md +3 -3
  78. package/docs/session-format.md +1 -1
  79. package/docs/tui.md +2 -0
  80. package/package.json +1 -1
@@ -0,0 +1,182 @@
1
+ /**
2
+ * config.json 的 JSON Schema(draft-07 子集),写到配置目录的 `config.schema.json` 给编辑器补全与校验
3
+ * (docs/providers.md「配置目录」)。规则与 schema.ts 的 `validateConfig` 一一对应:未知字段在那里是
4
+ * warning、这里是 `additionalProperties: false`;渠道引用(模型 `channels` 指向已定义的渠道)这类跨字段
5
+ * 规则 JSON Schema 表达不了,只在 `validateConfig` 里查。一致性由 json-schema.test.ts 的正反例守住。
6
+ */
7
+ import { WARMING_MODES } from "../ai/cache/types.js";
8
+ import { PERMISSION_MODES_STRICT_FIRST } from "../permissions/types.js";
9
+ import { HOOK_TIMEOUT_MAX_MS, THINKING_LEVELS } from "./schema.js";
10
+ import { CACHE_RETENTIONS, CHANNEL_NAME_PATTERN, CODEMODE_MODES, TOOLS_PRESETS_STRICT_FIRST, } from "./types.js";
11
+ export const CONFIG_SCHEMA_FILE = "config.schema.json";
12
+ export const CONFIG_SCHEMA_ID = "https://github.com/Owlbay/armadra-agent/config.schema.json";
13
+ const KNOWN_APIS = [
14
+ "openai-completions",
15
+ "openai-responses",
16
+ "anthropic-messages",
17
+ "google-generative-ai",
18
+ ];
19
+ function object(properties, extra = {}) {
20
+ return { type: "object", properties, additionalProperties: false, ...extra };
21
+ }
22
+ const str = (description) => ({
23
+ type: "string",
24
+ ...(description ? { description } : {}),
25
+ });
26
+ const bool = (description) => ({
27
+ type: "boolean",
28
+ ...(description ? { description } : {}),
29
+ });
30
+ const num = (minimum, maximum, description) => ({
31
+ type: "number",
32
+ ...(minimum !== undefined ? { minimum } : {}),
33
+ ...(maximum !== undefined ? { maximum } : {}),
34
+ ...(description ? { description } : {}),
35
+ });
36
+ const strings = { type: "array", items: { type: "string" } };
37
+ const stringRecord = { type: "object", additionalProperties: { type: "string" } };
38
+ const oneOf = (values, description) => ({
39
+ type: "string",
40
+ enum: [...values],
41
+ ...(description ? { description } : {}),
42
+ });
43
+ /** api 只查是字符串(与 validateConfig 一致),enum 给编辑器补全。 */
44
+ const api = {
45
+ type: "string",
46
+ description: "协议",
47
+ examples: KNOWN_APIS,
48
+ anyOf: [{ enum: KNOWN_APIS }, { type: "string" }],
49
+ };
50
+ const compat = {
51
+ type: "object",
52
+ description: "协议兼容开关(docs/providers.md「compat」)",
53
+ properties: {
54
+ sendPromptCacheKey: bool(),
55
+ sendSessionAffinityHeaders: bool(),
56
+ supportsLongCacheRetention: bool(),
57
+ supportsExplicitPromptCacheMode: bool(),
58
+ cacheReporting: oneOf(["auto", "silent", "reported"]),
59
+ },
60
+ };
61
+ function model(description) {
62
+ return {
63
+ type: "object",
64
+ description,
65
+ required: ["id"],
66
+ properties: {
67
+ id: str("模型 id(发给上游的名字)"),
68
+ name: str(),
69
+ api,
70
+ baseUrl: str("覆盖渠道 / 供应商的地址"),
71
+ contextWindow: num(1, undefined, "上下文 token;缺省从 models.dev 补,匹配不到不猜"),
72
+ maxTokens: num(1, undefined, "单次输出上限;缺省 min(models.dev, 64k) 或 8192"),
73
+ reasoning: bool(),
74
+ input: {
75
+ type: "array",
76
+ items: { enum: ["text", "image"] },
77
+ description: '["text"] 或 ["text", "image"](收图片)',
78
+ },
79
+ channels: { ...strings, description: "挂载的渠道,第一个是首选" },
80
+ modelsDev: {
81
+ description: 'models.dev 条目 "provider/model";false 关闭补全',
82
+ anyOf: [{ type: "string", pattern: "^[^/]+/.+$" }, { const: false }],
83
+ },
84
+ headers: stringRecord,
85
+ compat,
86
+ promptCache: object({ short: num(), long: num(), minTokens: num() }),
87
+ cost: { type: "object", description: "$/M token" },
88
+ thinkingLevelMap: { type: "object" },
89
+ samplingParams: { type: "object" },
90
+ },
91
+ };
92
+ }
93
+ const channel = object({
94
+ api,
95
+ baseUrl: str("接口地址"),
96
+ apiKey: str("$ENV / ${ENV} / !command / 字面量;缺省用供应商的 key"),
97
+ authHeader: { type: "object" },
98
+ headers: stringRecord,
99
+ compat,
100
+ }, { required: ["api", "baseUrl"] });
101
+ const provider = object({
102
+ name: str(),
103
+ api,
104
+ baseUrl: str(),
105
+ apiKey: str("$ENV / ${ENV} / !command / 字面量"),
106
+ envKeys: strings,
107
+ authHeader: { type: "object" },
108
+ headers: stringRecord,
109
+ compat,
110
+ requiresApiKey: bool(),
111
+ channels: {
112
+ type: "object",
113
+ description: "渠道:一个供应商下的多种接口",
114
+ minProperties: 1,
115
+ propertyNames: { pattern: CHANNEL_NAME_PATTERN.source },
116
+ additionalProperties: channel,
117
+ },
118
+ defaultChannel: str("首选渠道;缺省 channels 的第一个"),
119
+ models: { type: "array", items: model("自定义模型(同 id 整条替换)") },
120
+ modelOverrides: { type: "array", items: model("只改已有模型的元数据") },
121
+ });
122
+ /** 生成 config.json 的 JSON Schema。 */
123
+ export function buildConfigJsonSchema() {
124
+ return {
125
+ $schema: "http://json-schema.org/draft-07/schema#",
126
+ $id: CONFIG_SCHEMA_ID,
127
+ title: "ama config.json",
128
+ ...object({
129
+ $schema: str(),
130
+ version: { const: 1 },
131
+ defaultModel: str("provider/model 或 provider/model@channel"),
132
+ thinkingLevel: oneOf(THINKING_LEVELS),
133
+ providers: { type: "object", additionalProperties: provider },
134
+ permission: object({
135
+ mode: oneOf(PERMISSION_MODES_STRICT_FIRST),
136
+ allow: strings,
137
+ deny: strings,
138
+ builtinDeny: { anyOf: [{ type: "boolean" }, strings] },
139
+ }),
140
+ compaction: object({ enabled: bool(), reserveTokens: num(0), keepRecentTokens: num(0) }),
141
+ retry: object({
142
+ enabled: bool(),
143
+ maxRetries: num(0, 100),
144
+ baseDelayMs: num(0),
145
+ maxDelayMs: num(0),
146
+ }),
147
+ tools: object({
148
+ preset: oneOf(TOOLS_PRESETS_STRICT_FIRST),
149
+ default: strings,
150
+ maxToolResultChars: num(1),
151
+ bashTimeoutMs: num(1),
152
+ disabled: strings,
153
+ }),
154
+ codemode: object({
155
+ mode: oneOf(CODEMODE_MODES),
156
+ inlineBudget: num(0),
157
+ requireStrict: bool(),
158
+ }),
159
+ hooks: object({ timeoutMs: num(1, HOOK_TIMEOUT_MAX_MS) }),
160
+ ui: object({
161
+ theme: oneOf(["dark", "light"]),
162
+ markdown: bool(),
163
+ showThinking: oneOf(["full", "collapsed", "hidden"]),
164
+ tuiMode: oneOf(["regular"]),
165
+ quietStartup: oneOf(["normal", "header", "silent"]),
166
+ }),
167
+ skills: object({ dirs: strings }),
168
+ cache: object({
169
+ warming: oneOf(WARMING_MODES),
170
+ retention: oneOf(CACHE_RETENTIONS),
171
+ minSavingsUsd: num(0),
172
+ missNotices: bool(),
173
+ warmSubagents: bool(),
174
+ }),
175
+ }),
176
+ required: ["version"],
177
+ };
178
+ }
179
+ /** 写进文件的文本(2 空格缩进 + 换行结尾)。 */
180
+ export function configSchemaText() {
181
+ return `${JSON.stringify(buildConfigJsonSchema(), null, 2)}\n`;
182
+ }
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import type { AmaConfig, AuthFile, ProfileFile, TrustFile } from "./types.js";
9
9
  import type { HookConfig } from "../hooks/types.js";
10
- export type { AmaConfig, AuthFile, ProfileFile, TrustFile, TrustEntry, ProviderConfig, PermissionConfig, CompactionConfig, RetryConfig, ToolsConfig, ToolsPreset, CodemodeConfig, CodemodeMode, HooksSettings, UiConfig, SkillsConfig, CacheConfig, ModelConfig, ModelOverride, } from "./types.js";
10
+ export type { AmaConfig, AuthFile, ProfileFile, TrustFile, TrustEntry, ProviderConfig, PermissionConfig, CompactionConfig, RetryConfig, ToolsConfig, ToolsPreset, CodemodeConfig, CodemodeMode, HooksSettings, UiConfig, SkillsConfig, CacheConfig, ModelConfig, ModelOverride, ChannelConfig, } from "./types.js";
11
11
  export { CONFIG_FILE_VERSION } from "./types.js";
12
12
  export type { HookConfig } from "../hooks/types.js";
13
13
  export interface Diagnostic {
@@ -8,7 +8,7 @@
8
8
  import { HOOK_EVENTS } from "../hooks/types.js";
9
9
  import { PERMISSION_MODES_STRICT_FIRST } from "../permissions/types.js";
10
10
  import { WARMING_MODES } from "../ai/cache/types.js";
11
- import { CACHE_RETENTIONS, CODEMODE_MODES, TOOLS_PRESETS_STRICT_FIRST } from "./types.js";
11
+ import { CACHE_RETENTIONS, CHANNEL_NAME_PATTERN, CODEMODE_MODES, TOOLS_PRESETS_STRICT_FIRST, } from "./types.js";
12
12
  export { CONFIG_FILE_VERSION } from "./types.js";
13
13
  export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"];
14
14
  export const HOOK_TIMEOUT_MAX_MS = 600_000;
@@ -128,9 +128,12 @@ const PROVIDER_KEYS = [
128
128
  "headers",
129
129
  "compat",
130
130
  "requiresApiKey",
131
+ "channels",
132
+ "defaultChannel",
131
133
  "models",
132
134
  "modelOverrides",
133
135
  ];
136
+ const CHANNEL_KEYS = ["api", "baseUrl", "apiKey", "authHeader", "headers", "compat"];
134
137
  /** 第三波 §1.3 的缓存兼容开关(其余 compat 字段按协议各异,只查是对象)。 */
135
138
  const CACHE_COMPAT_FLAGS = [
136
139
  "sendPromptCacheKey",
@@ -138,7 +141,7 @@ const CACHE_COMPAT_FLAGS = [
138
141
  "supportsLongCacheRetention",
139
142
  "supportsExplicitPromptCacheMode",
140
143
  ];
141
- function checkModels(c, value, path) {
144
+ function checkModels(c, value, path, channels) {
142
145
  if (value === undefined)
143
146
  return;
144
147
  if (!Array.isArray(value)) {
@@ -157,6 +160,24 @@ function checkModels(c, value, path) {
157
160
  c.string(item, "api", p);
158
161
  c.string(item, "baseUrl", p);
159
162
  c.stringRecord(item, "headers", p);
163
+ const modelsDev = item["modelsDev"];
164
+ if (modelsDev !== undefined &&
165
+ modelsDev !== false &&
166
+ (typeof modelsDev !== "string" || !/^[^/]+\/.+$/.test(modelsDev))) {
167
+ c.error(join(p, "modelsDev"), `应为 "provider/model" 或 false`);
168
+ }
169
+ c.stringArray(item, "channels", p);
170
+ const used = item["channels"];
171
+ if (Array.isArray(used)) {
172
+ used.forEach((name, i) => {
173
+ if (typeof name !== "string")
174
+ return;
175
+ if (channels === undefined)
176
+ c.error(join(join(p, "channels"), i), "供应商没有 channels");
177
+ else if (!channels.has(name))
178
+ c.error(join(join(p, "channels"), i), `渠道 "${name}" 不存在(可用:${[...channels].join(", ")})`);
179
+ });
180
+ }
160
181
  const promptCache = item["promptCache"];
161
182
  if (promptCache !== undefined && c.object(promptCache, join(p, "promptCache"))) {
162
183
  const pp = join(p, "promptCache");
@@ -171,6 +192,51 @@ function checkModels(c, value, path) {
171
192
  }
172
193
  });
173
194
  }
195
+ function checkCompat(c, compat, path) {
196
+ if (compat === undefined || !c.object(compat, path))
197
+ return;
198
+ for (const key of CACHE_COMPAT_FLAGS)
199
+ c.boolean(compat, key, path);
200
+ c.oneOf(compat, "cacheReporting", path, ["auto", "silent", "reported"]);
201
+ }
202
+ /** 返回合法的渠道名集合;没有 `channels` 时 undefined。 */
203
+ function checkChannels(c, value, path) {
204
+ const channels = value["channels"];
205
+ if (channels === undefined)
206
+ return undefined;
207
+ const cp = join(path, "channels");
208
+ if (!c.object(channels, cp))
209
+ return new Set();
210
+ const names = new Set();
211
+ for (const [name, channel] of Object.entries(channels)) {
212
+ const p = join(cp, name);
213
+ if (!CHANNEL_NAME_PATTERN.test(name)) {
214
+ c.error(p, "渠道名只能含字母、数字、_ 与 -(不含 / 与 @),最长 32");
215
+ continue;
216
+ }
217
+ names.add(name);
218
+ if (!c.object(channel, p))
219
+ continue;
220
+ c.keys(channel, p, CHANNEL_KEYS);
221
+ c.string(channel, "api", p, true);
222
+ c.string(channel, "baseUrl", p, true);
223
+ c.string(channel, "apiKey", p);
224
+ c.stringRecord(channel, "headers", p);
225
+ if (channel["authHeader"] !== undefined)
226
+ c.object(channel["authHeader"], join(p, "authHeader"));
227
+ checkCompat(c, channel["compat"], join(p, "compat"));
228
+ }
229
+ if (names.size === 0 && Object.keys(channels).length === 0)
230
+ c.error(cp, "至少要有一个渠道");
231
+ const preferred = value["defaultChannel"];
232
+ if (preferred !== undefined) {
233
+ if (typeof preferred !== "string")
234
+ c.error(join(path, "defaultChannel"), "应为字符串");
235
+ else if (!names.has(preferred))
236
+ c.error(join(path, "defaultChannel"), `渠道 "${preferred}" 不存在`);
237
+ }
238
+ return names;
239
+ }
174
240
  function checkProvider(c, value, path) {
175
241
  if (!c.object(value, path))
176
242
  return;
@@ -180,17 +246,14 @@ function checkProvider(c, value, path) {
180
246
  c.stringArray(value, "envKeys", path);
181
247
  c.stringRecord(value, "headers", path);
182
248
  c.boolean(value, "requiresApiKey", path);
183
- const compat = value["compat"];
184
- if (compat !== undefined && c.object(compat, join(path, "compat"))) {
185
- const cp = join(path, "compat");
186
- for (const key of CACHE_COMPAT_FLAGS)
187
- c.boolean(compat, key, cp);
188
- c.oneOf(compat, "cacheReporting", cp, ["auto", "silent", "reported"]);
189
- }
249
+ checkCompat(c, value["compat"], join(path, "compat"));
190
250
  if (value["authHeader"] !== undefined)
191
251
  c.object(value["authHeader"], join(path, "authHeader"));
192
- checkModels(c, value["models"], join(path, "models"));
193
- checkModels(c, value["modelOverrides"], join(path, "modelOverrides"));
252
+ const channels = checkChannels(c, value, path);
253
+ if (channels === undefined && value["defaultChannel"] !== undefined)
254
+ c.error(join(path, "defaultChannel"), "没有 channels 时不能设 defaultChannel");
255
+ checkModels(c, value["models"], join(path, "models"), channels);
256
+ checkModels(c, value["modelOverrides"], join(path, "modelOverrides"), channels);
194
257
  }
195
258
  function checkSection(c, config, key, allowed, body) {
196
259
  const section = config[key];
@@ -16,13 +16,27 @@ export declare const CONFIG_FILE_VERSION: 1;
16
16
  * 自定义模型条目;缺省 maxTokens 8192、reasoning false、input ["text"],不猜 contextWindow。
17
17
  * `api` 缺省沿用供应商的协议。
18
18
  */
19
- export type ModelConfig = Partial<Omit<Model, "id" | "provider">> & {
20
- id: string;
21
- };
22
- /** 只改元数据的覆盖项(含模型级 `api`)。 */
23
- export type ModelOverride = Partial<Omit<Model, "id" | "provider">> & {
19
+ export type ModelConfig = Partial<Omit<Model, "id" | "provider" | "channel" | "channels">> & {
24
20
  id: string;
21
+ /** 挂载的渠道名,第一个是首选;缺省 `defaultChannel`(docs/providers.md「渠道」)。 */
22
+ channels?: string[];
23
+ /** models.dev 条目 `provider/model`(显式匹配);false 关闭 models.dev 补全。 */
24
+ modelsDev?: string | false;
25
25
  };
26
+ /** 只改元数据的覆盖项(含模型级 `api` 与 `channels`)。 */
27
+ export type ModelOverride = ModelConfig;
28
+ /** 渠道:协议 + 地址(+ 可选 key / headers / compat),缺省继承供应商级。 */
29
+ export interface ChannelConfig {
30
+ api: Api;
31
+ baseUrl: string;
32
+ /** 同供应商级 `apiKey` 的写法;缺省用供应商的 key。 */
33
+ apiKey?: string;
34
+ authHeader?: AuthHeader;
35
+ headers?: Record<string, string>;
36
+ compat?: ProviderCompat;
37
+ }
38
+ /** 渠道名:不含 `/` 与 `@`(`provider/model@channel`)。 */
39
+ export declare const CHANNEL_NAME_PATTERN: RegExp;
26
40
  export interface ProviderConfig {
27
41
  name?: string;
28
42
  /** 自定义供应商缺省 openai-completions。 */
@@ -35,6 +49,10 @@ export interface ProviderConfig {
35
49
  headers?: Record<string, string>;
36
50
  compat?: ProviderCompat;
37
51
  requiresApiKey?: boolean;
52
+ /** 多渠道(docs/providers.md「渠道」);不写时 `api` + `baseUrl` 是隐式的 `default` 渠道。 */
53
+ channels?: Record<string, ChannelConfig>;
54
+ /** 缺省 `channels` 的第一个键。 */
55
+ defaultChannel?: string;
38
56
  models?: ModelConfig[];
39
57
  modelOverrides?: ModelOverride[];
40
58
  }
@@ -9,6 +9,8 @@
9
9
  * 允许模型级 `api`(§2.3,同一中转的模型走不同协议),由 W3-B12 在注册表里生效。
10
10
  */
11
11
  export const CONFIG_FILE_VERSION = 1;
12
+ /** 渠道名:不含 `/` 与 `@`(`provider/model@channel`)。 */
13
+ export const CHANNEL_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/;
12
14
  /** 从严到宽:项目级只能把预设改成不比当前更宽的那个(coordinator 最严,codemode 最宽)。 */
13
15
  export const TOOLS_PRESETS_STRICT_FIRST = [
14
16
  "coordinator",
@@ -0,0 +1,27 @@
1
+ /**
2
+ * 用户侧的图片输入(docs/providers.md「图像输入」):`ama -p --image`、交互 / 行式界面里的 `@图片路径`
3
+ * 与粘贴 / 拖入的图片文件路径。
4
+ *
5
+ * - `--image` 与 `@路径` 是显式附件:文件不存在、不是图片、超过上限,或当前模型 `input` 不含 image
6
+ * → 报错,不发请求;
7
+ * - 不带 `@` 的词只在「以图片扩展名结尾且文件存在」时才算附件,当前模型不收图片时静默忽略(用户可能
8
+ * 只是在文字里提到一个文件);
9
+ * - 与 read 工具共用 image-file.ts 的 MIME 检测与 5 MB 上限。提示文字原样保留。
10
+ */
11
+ import type { AgentSession } from "../agent/types.js";
12
+ import type { ImageBlock, Model, ProviderRegistryApi } from "../ai/types.js";
13
+ export interface ImageRef {
14
+ path: string;
15
+ /** `@路径` 或 `--image`:显式附件。 */
16
+ explicit: boolean;
17
+ }
18
+ /** 从提示里找图片引用:`@路径`(可加引号)与以图片扩展名结尾、文件存在的词(拖入的路径常带 `\ `)。 */
19
+ export declare function findImageRefs(text: string, cwd: string): ImageRef[];
20
+ /** 当前会话的模型(查不到返回 undefined)。 */
21
+ export declare function sessionModel(providers: ProviderRegistryApi, session: AgentSession): Model | undefined;
22
+ /**
23
+ * 读出附件:显式附件遇到模型不收图片 → AmaError(提示换模型);隐式附件此时忽略。
24
+ */
25
+ export declare function loadPromptImages(refs: readonly ImageRef[], model: Model | undefined): Promise<ImageBlock[]>;
26
+ /** 提示 + 额外的显式图片路径(`--image`)→ 附件。 */
27
+ export declare function promptImages(text: string, extra: readonly string[], cwd: string, model: Model | undefined): Promise<ImageBlock[]>;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * 用户侧的图片输入(docs/providers.md「图像输入」):`ama -p --image`、交互 / 行式界面里的 `@图片路径`
3
+ * 与粘贴 / 拖入的图片文件路径。
4
+ *
5
+ * - `--image` 与 `@路径` 是显式附件:文件不存在、不是图片、超过上限,或当前模型 `input` 不含 image
6
+ * → 报错,不发请求;
7
+ * - 不带 `@` 的词只在「以图片扩展名结尾且文件存在」时才算附件,当前模型不收图片时静默忽略(用户可能
8
+ * 只是在文字里提到一个文件);
9
+ * - 与 read 工具共用 image-file.ts 的 MIME 检测与 5 MB 上限。提示文字原样保留。
10
+ */
11
+ import { existsSync } from "node:fs";
12
+ import { homedir } from "node:os";
13
+ import { resolve } from "node:path";
14
+ import { formatModelRef } from "../ai/providers/channels.js";
15
+ import { AmaError } from "../errors.js";
16
+ import { imageMimeFromPath, loadImageFile } from "../tools/image-file.js";
17
+ const TOKEN = /@?"([^"]+)"|@?'([^']+)'|((?:\\ |\S)+)/g;
18
+ function expand(path, cwd) {
19
+ const home = path === "~" || path.startsWith("~/") ? homedir() + path.slice(1) : path;
20
+ return resolve(cwd, home);
21
+ }
22
+ /** 从提示里找图片引用:`@路径`(可加引号)与以图片扩展名结尾、文件存在的词(拖入的路径常带 `\ `)。 */
23
+ export function findImageRefs(text, cwd) {
24
+ const out = [];
25
+ const seen = new Set();
26
+ for (const m of text.matchAll(TOKEN)) {
27
+ let token = m[1] ?? m[2] ?? m[3] ?? "";
28
+ const explicit = m[0].startsWith("@");
29
+ if (m[3] !== undefined && explicit)
30
+ token = token.slice(1);
31
+ token = token.replace(/\\ /g, " ").replace(/[,,。;;))]+$/, "");
32
+ if (token === "" || imageMimeFromPath(token) === undefined)
33
+ continue;
34
+ const path = expand(token, cwd);
35
+ if (!explicit && !existsSync(path))
36
+ continue;
37
+ if (seen.has(path))
38
+ continue;
39
+ seen.add(path);
40
+ out.push({ path, explicit });
41
+ }
42
+ return out;
43
+ }
44
+ /** 当前会话的模型(查不到返回 undefined)。 */
45
+ export function sessionModel(providers, session) {
46
+ const ref = session.state.model;
47
+ if (ref === undefined)
48
+ return undefined;
49
+ const found = providers.findModel(formatModelRef(ref));
50
+ return found.ok ? found.model : undefined;
51
+ }
52
+ /**
53
+ * 读出附件:显式附件遇到模型不收图片 → AmaError(提示换模型);隐式附件此时忽略。
54
+ */
55
+ export async function loadPromptImages(refs, model) {
56
+ const accepts = model === undefined || model.input.includes("image");
57
+ const wanted = refs.filter((ref) => ref.explicit || accepts);
58
+ if (wanted.length === 0)
59
+ return [];
60
+ if (!accepts) {
61
+ const name = model === undefined ? "当前模型" : `${model.provider}/${model.id}`;
62
+ throw new AmaError("invalid_arguments", `${name} 不接受图片输入(模型 input 没有 image);换一个支持图像的模型再试` +
63
+ `(ama models list 里标「图片」的,或在配置里给该模型写 "input": ["text", "image"])`);
64
+ }
65
+ const blocks = [];
66
+ for (const ref of wanted)
67
+ blocks.push((await loadImageFile(ref.path)).block);
68
+ return blocks;
69
+ }
70
+ /** 提示 + 额外的显式图片路径(`--image`)→ 附件。 */
71
+ export async function promptImages(text, extra, cwd, model) {
72
+ const refs = [
73
+ ...extra.map((path) => ({ path: expand(path, cwd), explicit: true })),
74
+ ...findImageRefs(text, cwd),
75
+ ];
76
+ const unique = refs.filter((ref, i) => refs.findIndex((r) => r.path === ref.path) === i);
77
+ return loadPromptImages(unique, model);
78
+ }
@@ -8,6 +8,7 @@
8
8
  * `/permissions`(当前模式、判定顺序与已加载规则);`/help` 追加这两条与按键说明。
9
9
  * - 不是命令(含模板与 `/skill:`)返回 false,调用方把整行当提示发出。
10
10
  */
11
+ import { formatModelRef } from "../../ai/providers/channels.js";
11
12
  import { AgentSessionImpl } from "../../agent/session.js";
12
13
  import { listSessions } from "../../cli/compose-store.js";
13
14
  import { BUILTIN_COMMANDS, parseSlash, runSlashCommand, } from "../commands-core.js";
@@ -139,9 +140,7 @@ async function handlePick(what, ui) {
139
140
  }
140
141
  case "thinking": {
141
142
  const model = session.state.model;
142
- const found = model === undefined
143
- ? undefined
144
- : ui.runtime.providers.findModel(`${model.provider}/${model.id}`);
143
+ const found = model === undefined ? undefined : ui.runtime.providers.findModel(formatModelRef(model));
145
144
  const reasoning = found?.ok === true ? found.model.reasoning : true;
146
145
  const picked = await ui.pick({
147
146
  title: "思考级别",
@@ -13,6 +13,7 @@
13
13
  * - 键位:Enter 发送(运行中 = steer),其余应用级键位见 `key-dispatch.ts`。
14
14
  * - 启动画面按 `ui.quietStartup`:normal 标题 + 模型 / 信任 / 资源清单,header 只有标题,silent 不输出。
15
15
  */
16
+ import { promptImages, sessionModel } from "../image-input.js";
16
17
  import { join } from "node:path";
17
18
  import { AgentSessionImpl } from "../../agent/session.js";
18
19
  import { currentSession, switchSession } from "../../cli/compose-session.js";
@@ -324,7 +325,10 @@ export function runInteractiveMode(runtime, context, options = {}) {
324
325
  const target = session;
325
326
  void target
326
327
  .waitForIdle()
327
- .then(() => target.prompt(text))
328
+ .then(async () => {
329
+ const images = await promptImages(text, [], target.state.cwd, sessionModel(runtime.providers, target));
330
+ return target.prompt(text, images.length > 0 ? { images } : {});
331
+ })
328
332
  .catch((error) => {
329
333
  if (isAmaError(error) && error.code === "busy") {
330
334
  void target.followUp(text).catch(() => undefined);
@@ -7,6 +7,7 @@
7
7
  * - 否则(管道):逐行读 stdin,每行依次执行(等上一条运行结束);没有审批 UI,ask → deny。
8
8
  * - 斜杠命令走 commands-core(与 B7 同一语义),`pick` 退化为列出候选。
9
9
  */
10
+ import { promptImages, sessionModel } from "../../image-input.js";
10
11
  import { currentSession, switchSession } from "../../../cli/compose-session.js";
11
12
  import { ExitCode } from "../../../cli/exit-codes.js";
12
13
  import { previewDisplayLines } from "../../../permissions/preview.js";
@@ -72,7 +73,8 @@ export async function runLineMode(runtime, context, options = {}) {
72
73
  return undefined;
73
74
  };
74
75
  const prompt = async (target, text) => {
75
- await target.prompt(text);
76
+ const images = await promptImages(text, [], target.state.cwd, sessionModel(runtime.providers, target));
77
+ await target.prompt(text, images.length > 0 ? { images } : {});
76
78
  printer.endLine();
77
79
  };
78
80
  if (!raw)
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * `modelItems` / `sessionItems` 同时供交互模式里的选择器(pickers.ts)使用。
9
9
  */
10
- import type { ProviderRegistryApi } from "../../ai/types.js";
10
+ import type { Model, ProviderRegistryApi } from "../../ai/types.js";
11
11
  import type { InteractiveUi } from "../../cli/deps.js";
12
12
  import type { SessionListItem } from "../../session/types.js";
13
13
  import { type Keybindings, type SelectItem, type Terminal, type Theme } from "../../tui.js";
@@ -19,7 +19,12 @@ export interface StartupUiOptions {
19
19
  /** 会话列表的相对时间基准(测试注入)。 */
20
20
  now?: () => number;
21
21
  }
22
- /** 模型按供应商分组;有 key(或本地)的供应商排前,组标题标 key 状态。 */
22
+ /** 选择器里模型的说明:名称(与 id 不同时)、上下文、`img`(收图片)。 */
23
+ export declare function modelDescription(model: Model): string | undefined;
24
+ /**
25
+ * 模型按「供应商 · 渠道」分组;有 key(或本地)的供应商排前,组标题标 key 状态。多渠道供应商的模型在
26
+ * 每个挂载的渠道下各出现一次,非首选渠道的值带 `@渠道`。
27
+ */
23
28
  export declare function modelItems(providers: ProviderRegistryApi): Promise<SelectItem[]>;
24
29
  /** `刚刚 / 5 分钟前 / 3 小时前 / 2 天前 / 2026-09-01`。 */
25
30
  export declare function relativeTime(iso: string, now: number): string;
@@ -102,7 +102,24 @@ function askText(r, prompt) {
102
102
  // ---------------------------------------------------------------------------
103
103
  // 列表项(选择器共用)
104
104
  // ---------------------------------------------------------------------------
105
- /** 模型按供应商分组;有 key(或本地)的供应商排前,组标题标 key 状态。 */
105
+ /** 选择器里模型的说明:名称(与 id 不同时)、上下文、`img`(收图片)。 */
106
+ export function modelDescription(model) {
107
+ const ctx = model.contextWindow;
108
+ const parts = [
109
+ model.name !== "" && model.name !== model.id ? model.name : undefined,
110
+ ctx === undefined
111
+ ? undefined
112
+ : ctx >= 1_000_000
113
+ ? `${Math.round(ctx / 100_000) / 10}M`
114
+ : `${Math.round(ctx / 1000)}k`,
115
+ model.input.includes("image") ? "img" : undefined,
116
+ ].filter((x) => x !== undefined);
117
+ return parts.length > 0 ? parts.join(" · ") : undefined;
118
+ }
119
+ /**
120
+ * 模型按「供应商 · 渠道」分组;有 key(或本地)的供应商排前,组标题标 key 状态。多渠道供应商的模型在
121
+ * 每个挂载的渠道下各出现一次,非首选渠道的值带 `@渠道`。
122
+ */
106
123
  export async function modelItems(providers) {
107
124
  const groups = [];
108
125
  for (const provider of providers.list()) {
@@ -117,16 +134,29 @@ export async function modelItems(providers) {
117
134
  ready = key.apiKey !== undefined;
118
135
  status = ready ? "key ✓" : "无 key";
119
136
  }
120
- const group = `${provider.id} · ${status}`;
121
- groups.push({
122
- ready,
123
- items: provider.models.map((model) => {
124
- const item = { value: `${provider.id}/${model.id}`, label: model.id, group };
125
- if (model.name !== "" && model.name !== model.id)
126
- item.description = model.name;
127
- return item;
128
- }),
129
- });
137
+ const item = (model, group, channel) => {
138
+ const suffix = channel !== undefined && channel !== model.channel ? `@${channel}` : "";
139
+ const out = {
140
+ value: `${provider.id}/${model.id}${suffix}`,
141
+ label: `${model.id}${suffix}`,
142
+ group,
143
+ };
144
+ const description = modelDescription(model);
145
+ if (description !== undefined)
146
+ out.description = description;
147
+ return out;
148
+ };
149
+ if (provider.channels === undefined) {
150
+ const group = `${provider.id} · ${status}`;
151
+ groups.push({ ready, items: provider.models.map((model) => item(model, group)) });
152
+ continue;
153
+ }
154
+ for (const channel of provider.channels) {
155
+ const group = `${provider.id} · ${channel.name} · ${status}`;
156
+ const models = provider.models.filter((m) => m.channels?.includes(channel.name));
157
+ if (models.length > 0)
158
+ groups.push({ ready, items: models.map((model) => item(model, group, channel.name)) });
159
+ }
130
160
  }
131
161
  return [...groups.filter((g) => g.ready), ...groups.filter((g) => !g.ready)].flatMap((g) => g.items);
132
162
  }
@@ -2,6 +2,8 @@
2
2
  * print 模式(`ama -p`,设计 §11.1 第 15–16 步)。[B6]
3
3
  *
4
4
  * - 提示 = 位置参数 + stdin 管道(stdin 非 TTY 时读到 EOF),两者都有时空一行拼接;都没有 → 用法错误 2。
5
+ * - 图片:`--image`(可重复)与提示里的 `@图片路径` / 图片文件路径作为附件(modes/image-input.ts);
6
+ * 显式附件遇到当前模型不收图片、文件不存在或超限 → 用法错误 2,不发请求。
5
7
  * - `--output-format text`(缺省):运行结束后输出最后一条助手文本;`json`:一个结果对象(文本、
6
8
  * 停止原因、用量、缓存命中率、[W3-C2] `cache` 统计(同 `get_session_stats.cache`)、全部条目);
7
9
  * `stream-json`:每个会话事件一行(线上形状同 RPC,含 `cache_miss` / `cache_warm` /