@armadra/agent 0.3.0 → 0.4.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.
- package/CHANGELOG.md +84 -0
- package/README.md +93 -44
- package/dist/agent/retry.d.ts +1 -1
- package/dist/agent/retry.js +2 -1
- package/dist/agent/session-cache.js +1 -1
- package/dist/agent/session-classifier.d.ts +18 -0
- package/dist/agent/session-classifier.js +103 -0
- package/dist/agent/session-core.d.ts +13 -0
- package/dist/agent/session-tools.js +24 -3
- package/dist/agent/session.d.ts +3 -0
- package/dist/agent/session.js +12 -3
- package/dist/agent/tool-runner.js +15 -2
- package/dist/agent/types.d.ts +9 -1
- package/dist/ai/apis/anthropic-messages.js +3 -2
- package/dist/ai/apis/google-generative-ai.js +3 -2
- package/dist/ai/apis/openai-completions.js +3 -2
- package/dist/ai/apis/openai-responses.js +3 -2
- package/dist/ai/http.d.ts +28 -6
- package/dist/ai/http.js +41 -8
- package/dist/ai/providers/registry.d.ts +8 -1
- package/dist/ai/providers/registry.js +30 -22
- package/dist/ai/providers/suggest.d.ts +18 -0
- package/dist/ai/providers/suggest.js +72 -0
- package/dist/ai/sse.d.ts +6 -2
- package/dist/ai/sse.js +22 -2
- package/dist/ai/types.d.ts +9 -2
- package/dist/bundle/ama.cjs +15279 -10151
- package/dist/cli/args.d.ts +19 -4
- package/dist/cli/args.js +83 -10
- package/dist/cli/bootstrap.js +27 -3
- package/dist/cli/codemode-notice.d.ts +20 -0
- package/dist/cli/codemode-notice.js +55 -0
- package/dist/cli/compose-session.d.ts +5 -0
- package/dist/cli/compose-session.js +32 -1
- package/dist/cli/compose-store.d.ts +1 -1
- package/dist/cli/compose-store.js +3 -1
- package/dist/cli/compose.d.ts +14 -4
- package/dist/cli/compose.js +29 -10
- package/dist/cli/default-model.d.ts +38 -1
- package/dist/cli/default-model.js +96 -9
- package/dist/cli/deps.d.ts +36 -0
- package/dist/cli/exit-codes.d.ts +2 -0
- package/dist/cli/exit-codes.js +3 -0
- package/dist/cli/fake-visibility.d.ts +13 -0
- package/dist/cli/fake-visibility.js +26 -0
- package/dist/cli/from-prompt.d.ts +18 -0
- package/dist/cli/from-prompt.js +49 -0
- package/dist/cli/main.d.ts +9 -2
- package/dist/cli/main.js +77 -3
- package/dist/cli/proxy.d.ts +51 -0
- package/dist/cli/proxy.js +135 -0
- package/dist/cli/startup-screen.d.ts +29 -0
- package/dist/cli/startup-screen.js +47 -0
- package/dist/cli/startup-steps.d.ts +1 -1
- package/dist/cli/startup-steps.js +14 -12
- package/dist/cli/subcommands/config.d.ts +19 -3
- package/dist/cli/subcommands/config.js +108 -21
- package/dist/cli/subcommands/context.js +4 -1
- package/dist/cli/subcommands/doctor.js +12 -1
- package/dist/cli/subcommands/init.js +2 -1
- package/dist/cli/subcommands/models-discover.d.ts +15 -9
- package/dist/cli/subcommands/models-discover.js +53 -46
- package/dist/cli/subcommands/probe-runner.d.ts +96 -0
- package/dist/cli/subcommands/probe-runner.js +264 -0
- package/dist/cli/subcommands/providers-probe.d.ts +34 -0
- package/dist/cli/subcommands/providers-probe.js +87 -0
- package/dist/cli/subcommands/providers.d.ts +3 -2
- package/dist/cli/subcommands/providers.js +48 -45
- package/dist/cli/subcommands/sessions-export.d.ts +10 -0
- package/dist/cli/subcommands/sessions-export.js +59 -0
- package/dist/cli/subcommands/sessions-search.d.ts +13 -0
- package/dist/cli/subcommands/sessions-search.js +103 -0
- package/dist/cli/subcommands/sessions.d.ts +3 -2
- package/dist/cli/subcommands/sessions.js +22 -1
- package/dist/cli/subcommands/stats.d.ts +17 -0
- package/dist/cli/subcommands/stats.js +198 -0
- package/dist/cli/system-prompt-arg.d.ts +11 -0
- package/dist/cli/system-prompt-arg.js +34 -0
- package/dist/codemode/modes.d.ts +4 -11
- package/dist/codemode/modes.js +5 -27
- package/dist/codemode/tool.d.ts +18 -14
- package/dist/codemode/tool.js +71 -28
- package/dist/config/init.d.ts +6 -2
- package/dist/config/init.js +12 -5
- package/dist/config/json-schema.d.ts +1 -0
- package/dist/config/json-schema.js +36 -3
- package/dist/config/key-docs.d.ts +22 -0
- package/dist/config/key-docs.js +105 -0
- package/dist/config/merge.d.ts +9 -8
- package/dist/config/merge.js +22 -7
- package/dist/config/schema.d.ts +1 -1
- package/dist/config/schema.js +24 -5
- package/dist/config/types.d.ts +34 -6
- package/dist/config/types.js +17 -1
- package/dist/modes/commands-core.js +6 -5
- package/dist/modes/interactive/approval-dialog.d.ts +21 -5
- package/dist/modes/interactive/approval-dialog.js +106 -27
- package/dist/modes/interactive/commands.d.ts +11 -2
- package/dist/modes/interactive/commands.js +56 -16
- package/dist/modes/interactive/interactive-mode.d.ts +2 -1
- package/dist/modes/interactive/interactive-mode.js +65 -81
- package/dist/modes/interactive/key-dispatch.d.ts +3 -1
- package/dist/modes/interactive/key-dispatch.js +5 -6
- package/dist/modes/interactive/line/line-mode.d.ts +1 -0
- package/dist/modes/interactive/line/line-mode.js +6 -4
- package/dist/modes/interactive/line/line-render.d.ts +4 -0
- package/dist/modes/interactive/line/line-render.js +28 -6
- package/dist/modes/interactive/message-view.d.ts +48 -9
- package/dist/modes/interactive/message-view.js +238 -44
- package/dist/modes/interactive/panels.d.ts +18 -0
- package/dist/modes/interactive/panels.js +143 -0
- package/dist/modes/interactive/pickers.d.ts +23 -2
- package/dist/modes/interactive/pickers.js +48 -15
- package/dist/modes/interactive/run-indicator.d.ts +51 -0
- package/dist/modes/interactive/run-indicator.js +189 -0
- package/dist/modes/interactive/startup-header.d.ts +40 -0
- package/dist/modes/interactive/startup-header.js +169 -0
- package/dist/modes/interactive/status-bar.d.ts +18 -15
- package/dist/modes/interactive/status-bar.js +98 -56
- package/dist/modes/interactive/tool-summary.d.ts +46 -0
- package/dist/modes/interactive/tool-summary.js +218 -0
- package/dist/modes/interactive/tool-view.d.ts +48 -15
- package/dist/modes/interactive/tool-view.js +203 -145
- package/dist/modes/print/print-mode.d.ts +31 -4
- package/dist/modes/print/print-mode.js +116 -7
- package/dist/modes/rpc/commands.js +5 -0
- package/dist/permissions/auto-safe.d.ts +60 -0
- package/dist/permissions/auto-safe.js +529 -0
- package/dist/permissions/classifier.d.ts +64 -0
- package/dist/permissions/classifier.js +184 -0
- package/dist/permissions/dangerous.d.ts +5 -0
- package/dist/permissions/dangerous.js +1 -1
- package/dist/permissions/modes.d.ts +30 -0
- package/dist/permissions/modes.js +78 -0
- package/dist/permissions/pipeline.d.ts +31 -4
- package/dist/permissions/pipeline.js +196 -6
- package/dist/permissions/protected.d.ts +19 -0
- package/dist/permissions/protected.js +74 -0
- package/dist/permissions/rules.js +3 -0
- package/dist/permissions/types.d.ts +50 -3
- package/dist/permissions/types.js +3 -0
- package/dist/sdk.d.ts +9 -3
- package/dist/sdk.js +10 -2
- package/dist/session/export.d.ts +32 -0
- package/dist/session/export.js +187 -0
- package/dist/session/redact.d.ts +15 -0
- package/dist/session/redact.js +55 -0
- package/dist/session/reuse.d.ts +33 -0
- package/dist/session/reuse.js +86 -0
- package/dist/session/scan.d.ts +34 -0
- package/dist/session/scan.js +140 -0
- package/dist/session/search.d.ts +52 -0
- package/dist/session/search.js +211 -0
- package/dist/session/stats-aggregate.d.ts +63 -0
- package/dist/session/stats-aggregate.js +163 -0
- package/dist/session/stats-index.d.ts +26 -0
- package/dist/session/stats-index.js +91 -0
- package/dist/session/stats-scan.d.ts +54 -0
- package/dist/session/stats-scan.js +236 -0
- package/dist/tools/presets.d.ts +35 -8
- package/dist/tools/presets.js +56 -17
- package/dist/tui/component.d.ts +8 -2
- package/dist/tui/component.js +3 -1
- package/dist/tui/components/box.d.ts +6 -1
- package/dist/tui/components/box.js +16 -6
- package/dist/tui/components/card.d.ts +23 -0
- package/dist/tui/components/card.js +37 -0
- package/dist/tui/components/editor-history.d.ts +6 -0
- package/dist/tui/components/editor-history.js +45 -0
- package/dist/tui/components/editor-paste.d.ts +1 -1
- package/dist/tui/components/editor-paste.js +4 -4
- package/dist/tui/components/editor.d.ts +11 -5
- package/dist/tui/components/editor.js +52 -58
- package/dist/tui/components/key-value.d.ts +3 -0
- package/dist/tui/components/key-value.js +16 -6
- package/dist/tui/components/loader.d.ts +37 -7
- package/dist/tui/components/loader.js +84 -21
- package/dist/tui/components/markdown.d.ts +5 -1
- package/dist/tui/components/markdown.js +45 -21
- package/dist/tui/components/meter.d.ts +3 -3
- package/dist/tui/components/meter.js +13 -11
- package/dist/tui/components/select-list.d.ts +23 -1
- package/dist/tui/components/select-list.js +76 -13
- package/dist/tui/glyphs.d.ts +68 -0
- package/dist/tui/glyphs.js +114 -0
- package/dist/tui/theme.d.ts +30 -6
- package/dist/tui/theme.js +103 -17
- package/dist/tui.d.ts +4 -2
- package/dist/tui.js +3 -1
- package/docs/codemode.md +23 -9
- package/docs/hooks.md +10 -10
- package/docs/permissions.md +148 -0
- package/docs/providers.md +33 -10
- package/docs/rpc.md +36 -36
- package/docs/session-format.md +2 -1
- package/docs/sessions.md +134 -0
- package/docs/tui.md +139 -62
- package/package.json +3 -1
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ama providers add|refresh --probe` 的逐渠道探测:任务是「模型 × 渠道」,有界并发执行(见
|
|
3
|
+
* probe-runner.ts);每个模型的所有渠道探完后按模型顺序打印一行,不按完成顺序。
|
|
4
|
+
*/
|
|
5
|
+
import type { ProviderRegistryApi } from "../../ai/types.js";
|
|
6
|
+
import type { CliIo } from "../deps.js";
|
|
7
|
+
import type { CandidateChannel } from "./providers-plan.js";
|
|
8
|
+
export interface ChannelProbeResult {
|
|
9
|
+
/** 探测成功的渠道(按渠道尝试顺序)。 */
|
|
10
|
+
ok: string[];
|
|
11
|
+
/** 最后一个失败渠道的原因(首行,≤ 120 字符)。 */
|
|
12
|
+
error?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface ChannelProbeInput {
|
|
15
|
+
io: Pick<CliIo, "stdout" | "stderr" | "stdoutIsTTY">;
|
|
16
|
+
providerId: string;
|
|
17
|
+
ids: readonly string[];
|
|
18
|
+
/** 每个模型要试的渠道名(顺序即输出顺序)。 */
|
|
19
|
+
channelsFor: (id: string) => readonly string[];
|
|
20
|
+
candidates: readonly CandidateChannel[];
|
|
21
|
+
registry: ProviderRegistryApi;
|
|
22
|
+
apiKey: string | undefined;
|
|
23
|
+
concurrency: number;
|
|
24
|
+
timeoutMs: number;
|
|
25
|
+
/** 测试用:429 重试前的退避。 */
|
|
26
|
+
retryDelayMs?: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 探测全部「模型 × 渠道」。返回有结论的模型(全部渠道探完,或因停止没探完但已有成功渠道)与停止原因。
|
|
30
|
+
*/
|
|
31
|
+
export declare function probeChannels(input: ChannelProbeInput): Promise<{
|
|
32
|
+
results: Map<string, ChannelProbeResult>;
|
|
33
|
+
stopped?: string;
|
|
34
|
+
}>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ama providers add|refresh --probe` 的逐渠道探测:任务是「模型 × 渠道」,有界并发执行(见
|
|
3
|
+
* probe-runner.ts);每个模型的所有渠道探完后按模型顺序打印一行,不按完成顺序。
|
|
4
|
+
*/
|
|
5
|
+
import { withCustomDefaults } from "../../ai/providers/catalog.js";
|
|
6
|
+
import { materializeModel } from "../../ai/providers/registry.js";
|
|
7
|
+
import { ProbeProgress, ProbeScheduler } from "./probe-runner.js";
|
|
8
|
+
/** 渠道的探测用供应商数据(key 由调用方传入)。 */
|
|
9
|
+
function probeProvider(id, channel) {
|
|
10
|
+
return {
|
|
11
|
+
id,
|
|
12
|
+
name: id,
|
|
13
|
+
api: channel.api,
|
|
14
|
+
baseUrl: channel.baseUrl,
|
|
15
|
+
envKeys: [],
|
|
16
|
+
models: [],
|
|
17
|
+
requiresApiKey: true,
|
|
18
|
+
builtin: false,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const firstLine = (error) => error.split("\n")[0]?.slice(0, 120) ?? error;
|
|
22
|
+
/**
|
|
23
|
+
* 探测全部「模型 × 渠道」。返回有结论的模型(全部渠道探完,或因停止没探完但已有成功渠道)与停止原因。
|
|
24
|
+
*/
|
|
25
|
+
export async function probeChannels(input) {
|
|
26
|
+
const { io } = input;
|
|
27
|
+
const tasks = input.ids.flatMap((id) => input.channelsFor(id).map((name) => ({ id, name })));
|
|
28
|
+
const pending = new Map(input.ids.map((id) => [id, input.channelsFor(id).length]));
|
|
29
|
+
const outcomes = new Map();
|
|
30
|
+
const results = new Map();
|
|
31
|
+
const progress = new ProbeProgress(io.stdout, io.stdoutIsTTY, tasks.length);
|
|
32
|
+
const scheduler = new ProbeScheduler(input.registry, input.apiKey, {
|
|
33
|
+
concurrency: input.concurrency,
|
|
34
|
+
timeoutMs: input.timeoutMs,
|
|
35
|
+
retryDelayMs: input.retryDelayMs,
|
|
36
|
+
onThrottle: (n) => progress.line(` 遇到 429 限流,并发降到 ${n},稍后重试一次\n`),
|
|
37
|
+
onRecover: (n) => progress.line(` 一段时间没再限流,并发回升到 ${n}\n`),
|
|
38
|
+
});
|
|
39
|
+
let printed = 0;
|
|
40
|
+
/** 按模型顺序输出:前面的模型都探完才输出后面的。 */
|
|
41
|
+
const flush = (final) => {
|
|
42
|
+
while (printed < input.ids.length) {
|
|
43
|
+
const id = input.ids[printed];
|
|
44
|
+
if (!final && (pending.get(id) ?? 0) > 0)
|
|
45
|
+
return;
|
|
46
|
+
printed++;
|
|
47
|
+
const result = { ok: [] };
|
|
48
|
+
const failed = [];
|
|
49
|
+
let complete = true;
|
|
50
|
+
for (const name of input.channelsFor(id)) {
|
|
51
|
+
const outcome = outcomes.get(`${id}\n${name}`);
|
|
52
|
+
if (outcome === undefined || outcome.aborted)
|
|
53
|
+
complete = false;
|
|
54
|
+
else if (outcome.error === undefined)
|
|
55
|
+
result.ok.push(name);
|
|
56
|
+
else {
|
|
57
|
+
result.error = firstLine(outcome.error);
|
|
58
|
+
failed.push(`${name}:${result.error}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!complete && result.ok.length === 0)
|
|
62
|
+
continue;
|
|
63
|
+
results.set(id, result);
|
|
64
|
+
progress.line(` ${id} ${result.ok.length > 0 ? result.ok.join(", ") : "全部失败"}` +
|
|
65
|
+
`${failed.length > 0 ? `(失败 ${failed.join(";")})` : ""}` +
|
|
66
|
+
`${complete ? "" : "(探测提前停止,部分渠道未探)"}\n`);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
await scheduler.run(tasks.length, async (index) => {
|
|
70
|
+
const { id, name } = tasks[index];
|
|
71
|
+
const channel = input.candidates.find((c) => c.name === name);
|
|
72
|
+
const provider = probeProvider(input.providerId, channel);
|
|
73
|
+
const model = materializeModel(withCustomDefaults({ id }, input.providerId, channel.api), provider);
|
|
74
|
+
return scheduler.probe(model);
|
|
75
|
+
}, (index, outcome) => {
|
|
76
|
+
const { id, name } = tasks[index];
|
|
77
|
+
outcomes.set(`${id}\n${name}`, outcome);
|
|
78
|
+
pending.set(id, (pending.get(id) ?? 1) - 1);
|
|
79
|
+
if (!outcome.aborted)
|
|
80
|
+
progress.tick();
|
|
81
|
+
flush(false);
|
|
82
|
+
});
|
|
83
|
+
flush(true);
|
|
84
|
+
progress.finish();
|
|
85
|
+
const stopped = scheduler.stopped;
|
|
86
|
+
return { results, ...(stopped !== undefined ? { stopped } : {}) };
|
|
87
|
+
}
|
|
@@ -7,14 +7,15 @@
|
|
|
7
7
|
* - refresh:重拉 `/models` 与 models.dev,只追加新模型;已下架的只提示。
|
|
8
8
|
* - list / channels:供应商 → 渠道 → 模型数与 key 来源(从不显示 key)。remove:删配置与 auth.json 条目。
|
|
9
9
|
*
|
|
10
|
-
* 计费:`--probe`
|
|
10
|
+
* 计费:`--probe` 每模型每渠道一次最小请求(并发执行,见到首个流事件即判可用并断开,见
|
|
11
|
+
* probe-runner.ts);执行前打印预估,TTY 问 y/N,非 TTY 必须 `--yes`。
|
|
11
12
|
*/
|
|
12
13
|
import type { AmaConfig } from "../../config/types.js";
|
|
13
14
|
import type { CliIo, RuntimeDeps } from "../deps.js";
|
|
14
15
|
import { type UserLevel } from "./context.js";
|
|
15
16
|
export declare const DEFAULT_MAX_REQUESTS = 60;
|
|
16
17
|
export declare const DEFAULT_LIMIT = 30;
|
|
17
|
-
export declare const PROVIDERS_USAGE = "\u7528\u6CD5\uFF1Aama providers add <id> --base-url <url> [--channel <\u540D\u5B57>=<\u534F\u8BAE>@<\u5730\u5740> \u2026]\n [--api openai-completions|openai-responses|anthropic-messages|auto]\n [--key-env <VAR>] [--probe] [--limit N] [--probe-models a,b,\u2026]\n [--max-requests N] [--prefer chat,responses,messages]
|
|
18
|
+
export declare const PROVIDERS_USAGE = "\u7528\u6CD5\uFF1Aama providers add <id> --base-url <url> [--channel <\u540D\u5B57>=<\u534F\u8BAE>@<\u5730\u5740> \u2026]\n [--api openai-completions|openai-responses|anthropic-messages|auto]\n [--key-env <VAR>] [--probe] [--limit N] [--probe-models a,b,\u2026]\n [--max-requests N] [--concurrency N] [--probe-timeout ms]\n [--prefer chat,responses,messages] [--include-no-tools] [--yes]\n ama providers list\n ama providers channels <id>\n ama providers remove <id>\n ama providers refresh <id> [--probe] [--limit N] [--probe-models a,b,\u2026] [--max-requests N]\n [--concurrency N] [--probe-timeout ms] [--yes]\n";
|
|
18
19
|
export interface Ctx {
|
|
19
20
|
io: CliIo;
|
|
20
21
|
deps: Pick<RuntimeDeps, "providers">;
|
|
@@ -7,36 +7,39 @@
|
|
|
7
7
|
* - refresh:重拉 `/models` 与 models.dev,只追加新模型;已下架的只提示。
|
|
8
8
|
* - list / channels:供应商 → 渠道 → 模型数与 key 来源(从不显示 key)。remove:删配置与 auth.json 条目。
|
|
9
9
|
*
|
|
10
|
-
* 计费:`--probe`
|
|
10
|
+
* 计费:`--probe` 每模型每渠道一次最小请求(并发执行,见到首个流事件即判可用并断开,见
|
|
11
|
+
* probe-runner.ts);执行前打印预估,TTY 问 y/N,非 TTY 必须 `--yes`。
|
|
11
12
|
*/
|
|
12
13
|
import { existsSync } from "node:fs";
|
|
13
14
|
import { createInterface } from "node:readline/promises";
|
|
14
|
-
import { withCustomDefaults } from "../../ai/providers/catalog.js";
|
|
15
15
|
import { describeRefresh, refreshModelsDev } from "../../ai/providers/models-dev-cache.js";
|
|
16
16
|
import { modelsDevFields } from "../../ai/providers/models-dev.js";
|
|
17
|
-
import { materializeModel } from "../../ai/providers/registry.js";
|
|
18
17
|
import { PROVIDER_ID_PATTERN, setAuthKey } from "../../config/auth-file.js";
|
|
19
18
|
import { loadConfigFile } from "../../config/load.js";
|
|
20
19
|
import { CONFIG_FILE_VERSION } from "../../config/types.js";
|
|
21
20
|
import { writeConfigFile } from "../../config/write.js";
|
|
22
21
|
import { parseSubArgs, UsageError } from "../args.js";
|
|
22
|
+
import { pickByPrice } from "../default-model.js";
|
|
23
23
|
import { ExitCode } from "../exit-codes.js";
|
|
24
24
|
import { extractKey } from "./auth.js";
|
|
25
25
|
import { listChannels, listProviders, removeProvider } from "./providers-list.js";
|
|
26
26
|
import { buildRegistry, loadUserLevel } from "./context.js";
|
|
27
|
-
import {
|
|
27
|
+
import { DISCOVER_TIMEOUT_MS } from "./models-discover.js";
|
|
28
|
+
import { describeProbePlan, parseProbeTuning } from "./probe-runner.js";
|
|
29
|
+
import { probeChannels } from "./providers-probe.js";
|
|
28
30
|
import { DEFAULT_PREFER, capRequests, defaultChannels, hintedChannels, listingTarget, mergeProvider, orderChannels, parseChannelSpec, parseModelList, probeSelection, renderTable, } from "./providers-plan.js";
|
|
29
31
|
export const DEFAULT_MAX_REQUESTS = 60;
|
|
30
32
|
export const DEFAULT_LIMIT = 30;
|
|
31
33
|
export const PROVIDERS_USAGE = `用法:ama providers add <id> --base-url <url> [--channel <名字>=<协议>@<地址> …]
|
|
32
34
|
[--api openai-completions|openai-responses|anthropic-messages|auto]
|
|
33
35
|
[--key-env <VAR>] [--probe] [--limit N] [--probe-models a,b,…]
|
|
34
|
-
[--max-requests N] [--
|
|
35
|
-
[--include-no-tools] [--yes]
|
|
36
|
+
[--max-requests N] [--concurrency N] [--probe-timeout ms]
|
|
37
|
+
[--prefer chat,responses,messages] [--include-no-tools] [--yes]
|
|
36
38
|
ama providers list
|
|
37
39
|
ama providers channels <id>
|
|
38
40
|
ama providers remove <id>
|
|
39
|
-
ama providers refresh <id> [--probe] [--limit N] [--probe-models a,b,…] [--max-requests N]
|
|
41
|
+
ama providers refresh <id> [--probe] [--limit N] [--probe-models a,b,…] [--max-requests N]
|
|
42
|
+
[--concurrency N] [--probe-timeout ms] [--yes]
|
|
40
43
|
`;
|
|
41
44
|
const VALUE_OPTIONS = [
|
|
42
45
|
"base-url",
|
|
@@ -45,6 +48,8 @@ const VALUE_OPTIONS = [
|
|
|
45
48
|
"limit",
|
|
46
49
|
"probe-models",
|
|
47
50
|
"max-requests",
|
|
51
|
+
"concurrency",
|
|
52
|
+
"probe-timeout",
|
|
48
53
|
"prefer",
|
|
49
54
|
"name",
|
|
50
55
|
"profile",
|
|
@@ -119,19 +124,6 @@ function channelsOfConfig(config) {
|
|
|
119
124
|
const api = config.api ?? "openai-completions";
|
|
120
125
|
return [{ name: "default", api, baseUrl: config.baseUrl }];
|
|
121
126
|
}
|
|
122
|
-
/** 渠道的探测用供应商数据(key 由调用方传入)。 */
|
|
123
|
-
function probeProvider(id, channel) {
|
|
124
|
-
return {
|
|
125
|
-
id,
|
|
126
|
-
name: id,
|
|
127
|
-
api: channel.api,
|
|
128
|
-
baseUrl: channel.baseUrl,
|
|
129
|
-
envKeys: [],
|
|
130
|
-
models: [],
|
|
131
|
-
requiresApiKey: true,
|
|
132
|
-
builtin: false,
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
127
|
/** 逐模型定渠道(含探测);返回各模型的计划与是否因鉴权 / 限流提前停止。 */
|
|
136
128
|
async function plan(ctx, input) {
|
|
137
129
|
const { io } = ctx;
|
|
@@ -151,36 +143,28 @@ async function plan(ctx, input) {
|
|
|
151
143
|
const selected = probeSelection(fresh.map((m) => m.id).filter(probeable), intValue(ctx, "limit", DEFAULT_LIMIT), only);
|
|
152
144
|
const max = intValue(ctx, "max-requests", DEFAULT_MAX_REQUESTS);
|
|
153
145
|
const capped = capRequests(selected, tryChannels, max);
|
|
146
|
+
const tuning = parseProbeTuning(ctx.values);
|
|
154
147
|
io.stdout(`\n探测:${capped.ids.length} 个模型、${capped.requests} 次最小请求(每模型每渠道 1 次,上限 ${max})` +
|
|
155
|
-
`${capped.dropped > 0 ? `;另有 ${capped.dropped} 个超出上限未探测` : ""}\n`
|
|
148
|
+
`${capped.dropped > 0 ? `;另有 ${capped.dropped} 个超出上限未探测` : ""}\n` +
|
|
149
|
+
`${describeProbePlan(capped.requests, tuning.concurrency, tuning.timeoutMs)}\n`);
|
|
156
150
|
const ok = await approved(ctx, `将发 ${capped.requests} 次计费请求并写入 ${ctx.level.userConfigPath}`);
|
|
157
151
|
if (ok === "usage")
|
|
158
152
|
return { plans: [], cancelled: ExitCode.Usage };
|
|
159
153
|
if (!ok)
|
|
160
154
|
return { plans: [], cancelled: ExitCode.Ok };
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
155
|
+
const probed = await probeChannels({
|
|
156
|
+
io,
|
|
157
|
+
providerId: input.id,
|
|
158
|
+
ids: capped.ids,
|
|
159
|
+
channelsFor: tryChannels,
|
|
160
|
+
candidates: input.candidates,
|
|
161
|
+
registry: input.registry,
|
|
162
|
+
apiKey: input.apiKey,
|
|
163
|
+
...tuning,
|
|
164
|
+
});
|
|
165
|
+
for (const [id, result] of probed.results)
|
|
164
166
|
results.set(id, result);
|
|
165
|
-
|
|
166
|
-
const channel = input.candidates.find((c) => c.name === name);
|
|
167
|
-
const provider = probeProvider(input.id, channel);
|
|
168
|
-
const model = materializeModel(withCustomDefaults({ id }, input.id, channel.api), provider);
|
|
169
|
-
const error = await attempt(input.registry, model, input.apiKey);
|
|
170
|
-
if (error === undefined)
|
|
171
|
-
result.ok.push(name);
|
|
172
|
-
else {
|
|
173
|
-
result.error = error.split("\n")[0]?.slice(0, 120) ?? error;
|
|
174
|
-
failed.push(`${name}:${result.error}`);
|
|
175
|
-
if (FATAL_STATUS.test(error)) {
|
|
176
|
-
stopped = error;
|
|
177
|
-
break outer;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
io.stdout(` ${id} ${result.ok.length > 0 ? result.ok.join(", ") : "全部失败"}` +
|
|
182
|
-
`${failed.length > 0 ? `(失败 ${failed.join(";")})` : ""}\n`);
|
|
183
|
-
}
|
|
167
|
+
stopped = probed.stopped;
|
|
184
168
|
}
|
|
185
169
|
const includeNoTools = ctx.flags.has("include-no-tools");
|
|
186
170
|
const plans = input.listed.map((listed) => {
|
|
@@ -314,9 +298,25 @@ async function add(ctx, id, refresh) {
|
|
|
314
298
|
prefer: list(ctx.values.get("prefer")) ?? [...DEFAULT_PREFER],
|
|
315
299
|
});
|
|
316
300
|
const keyNote = key.store !== undefined ? `;key → ${level.authFile}(0600)` : "";
|
|
301
|
+
// 还没有缺省模型时按价格规则挑一个写进 defaultModel(default-model.ts 的 pickByPrice);
|
|
302
|
+
// 探测过时只在探测通过的模型里挑
|
|
303
|
+
const verified = writable.filter((p) => p.status === "ok");
|
|
304
|
+
const defaultPick = level.merged.config.defaultModel === undefined && config.defaultModel === undefined
|
|
305
|
+
? pickByPrice((verified.length > 0 ? verified : writable).map((p) => ({
|
|
306
|
+
id: p.id,
|
|
307
|
+
contextWindow: p.fields.contextWindow,
|
|
308
|
+
inputCost: p.fields.cost?.input,
|
|
309
|
+
toolCall: p.fields.toolCall,
|
|
310
|
+
})))
|
|
311
|
+
: undefined;
|
|
312
|
+
const defaultNote = defaultPick !== undefined ? `;defaultModel → ${id}/${defaultPick.id}` : "";
|
|
317
313
|
const summary = `\n将写入 ${level.userConfigPath}:${id} 新增渠道 ${merged.addedChannels.join(", ") || "无"},` +
|
|
318
|
-
`新增模型 ${merged.addedModels.length} 个${keyNote}`;
|
|
314
|
+
`新增模型 ${merged.addedModels.length} 个${keyNote}${defaultNote}`;
|
|
319
315
|
io.stdout(`${summary}\n`);
|
|
316
|
+
if (defaultPick !== undefined)
|
|
317
|
+
io.stdout(` 选 ${defaultPick.id}:${defaultPick.reason}\n`);
|
|
318
|
+
else if (level.merged.config.defaultModel === undefined && writable.length > 0)
|
|
319
|
+
io.stdout(" 未设置 defaultModel:没有同时支持工具调用、上下文 ≥ 64k 且有价格的模型;用 ama config edit 设置\n");
|
|
320
320
|
if (merged.addedModels.length === 0 &&
|
|
321
321
|
merged.addedChannels.length === 0 &&
|
|
322
322
|
key.store === undefined) {
|
|
@@ -334,12 +334,15 @@ async function add(ctx, id, refresh) {
|
|
|
334
334
|
}
|
|
335
335
|
const providers = (config.providers ??= {});
|
|
336
336
|
providers[id] = merged.config;
|
|
337
|
+
if (defaultPick !== undefined)
|
|
338
|
+
config.defaultModel = `${id}/${defaultPick.id}`;
|
|
337
339
|
const backup = existsSync(level.userConfigPath);
|
|
338
340
|
writeConfigFile(level.userConfigPath, config, { backup: true });
|
|
339
341
|
if (key.store !== undefined)
|
|
340
342
|
setAuthKey(level.authFile, id, key.store);
|
|
341
343
|
io.stdout(`已写入 ${level.userConfigPath}${backup ? "(原文件备份为 config.json.bak)" : ""}\n`);
|
|
342
|
-
const sample =
|
|
344
|
+
const sample = defaultPick?.id ??
|
|
345
|
+
result.plans.find((p) => p.status === "ok" && merged.addedModels.includes(p.id))?.id ??
|
|
343
346
|
merged.addedModels[0];
|
|
344
347
|
if (sample !== undefined)
|
|
345
348
|
io.stdout(`试试:ama -p "hi" --model ${id}/${sample}\n`);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ama sessions export <id>`:导出会话(只读,不加锁;正在运行的会话也能导)。[W4-D]
|
|
3
|
+
*
|
|
4
|
+
* - `--format md|json|jsonl`(缺省 md);`--branch leaf|all`(缺省 leaf = 当前分支);
|
|
5
|
+
* - `--output <文件>`:写文件(0600,已存在则覆盖),否则写 stdout;
|
|
6
|
+
* - 导出内容先脱敏(session/redact.ts),格式说明见 docs/sessions.md「导出」。
|
|
7
|
+
*/
|
|
8
|
+
import type { CliIo } from "../deps.js";
|
|
9
|
+
export declare const SESSIONS_EXPORT_USAGE = "\u7528\u6CD5\uFF1Aama sessions export <id> [--format md|json|jsonl] [--output <\u6587\u4EF6>] [--branch leaf|all]\n [--session-dir <\u76EE\u5F55>]\n";
|
|
10
|
+
export declare function runSessionsExport(argv: readonly string[], io: CliIo): Promise<number>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ama sessions export <id>`:导出会话(只读,不加锁;正在运行的会话也能导)。[W4-D]
|
|
3
|
+
*
|
|
4
|
+
* - `--format md|json|jsonl`(缺省 md);`--branch leaf|all`(缺省 leaf = 当前分支);
|
|
5
|
+
* - `--output <文件>`:写文件(0600,已存在则覆盖),否则写 stdout;
|
|
6
|
+
* - 导出内容先脱敏(session/redact.ts),格式说明见 docs/sessions.md「导出」。
|
|
7
|
+
*/
|
|
8
|
+
import { writeFileSync } from "node:fs";
|
|
9
|
+
import { join, resolve } from "node:path";
|
|
10
|
+
import { expandHome, resolveDataDir } from "../../config/paths.js";
|
|
11
|
+
import { renderExport } from "../../session/export.js";
|
|
12
|
+
import { findSessionFileReadOnly, readSessionReadOnly } from "../../session/scan.js";
|
|
13
|
+
import { parseSubArgs, UsageError } from "../args.js";
|
|
14
|
+
import { ExitCode } from "../exit-codes.js";
|
|
15
|
+
export const SESSIONS_EXPORT_USAGE = `用法:ama sessions export <id> [--format md|json|jsonl] [--output <文件>] [--branch leaf|all]
|
|
16
|
+
[--session-dir <目录>]
|
|
17
|
+
`;
|
|
18
|
+
const FORMATS = ["md", "json", "jsonl"];
|
|
19
|
+
const BRANCHES = ["leaf", "all"];
|
|
20
|
+
export async function runSessionsExport(argv, io) {
|
|
21
|
+
const { positionals, values, flags } = parseSubArgs(argv, [
|
|
22
|
+
"format",
|
|
23
|
+
"output",
|
|
24
|
+
"branch",
|
|
25
|
+
"session-dir",
|
|
26
|
+
]);
|
|
27
|
+
if (flags.has("help")) {
|
|
28
|
+
io.stdout(SESSIONS_EXPORT_USAGE);
|
|
29
|
+
return ExitCode.Ok;
|
|
30
|
+
}
|
|
31
|
+
const [id, extra] = positionals;
|
|
32
|
+
if (id === undefined)
|
|
33
|
+
throw new UsageError("ama sessions export 需要 <id>");
|
|
34
|
+
if (extra !== undefined)
|
|
35
|
+
throw new UsageError(`多余的参数:${extra}`);
|
|
36
|
+
const format = (values.get("format") ?? "md");
|
|
37
|
+
if (!FORMATS.includes(format)) {
|
|
38
|
+
throw new UsageError(`--format 的取值应为 ${FORMATS.join(" | ")}(收到 ${format})`);
|
|
39
|
+
}
|
|
40
|
+
const branch = (values.get("branch") ?? "leaf");
|
|
41
|
+
if (!BRANCHES.includes(branch)) {
|
|
42
|
+
throw new UsageError(`--branch 的取值应为 ${BRANCHES.join(" | ")}(收到 ${branch})`);
|
|
43
|
+
}
|
|
44
|
+
const dirFlag = values.get("session-dir");
|
|
45
|
+
const root = dirFlag !== undefined
|
|
46
|
+
? resolve(io.cwd, expandHome(dirFlag, { env: io.env }))
|
|
47
|
+
: join(resolveDataDir({ env: io.env }), "sessions");
|
|
48
|
+
const session = readSessionReadOnly(findSessionFileReadOnly(root, id, io.cwd));
|
|
49
|
+
const text = renderExport(session, format, branch);
|
|
50
|
+
const output = values.get("output");
|
|
51
|
+
if (output === undefined) {
|
|
52
|
+
io.stdout(text);
|
|
53
|
+
return ExitCode.Ok;
|
|
54
|
+
}
|
|
55
|
+
const target = resolve(io.cwd, expandHome(output, { env: io.env }));
|
|
56
|
+
writeFileSync(target, text, { mode: 0o600 });
|
|
57
|
+
io.stderr(`已导出到 ${target}\n`);
|
|
58
|
+
return ExitCode.Ok;
|
|
59
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ama sessions search <关键词|/正则/>`:跨会话全文检索(只读)。[W4-D]
|
|
3
|
+
*
|
|
4
|
+
* - 范围:缺省当前目录的会话,`--all` 全部;`--role user|assistant|tool`(可逗号分隔多个);
|
|
5
|
+
* `--since 7d|today|YYYY-MM-DD`;`--limit N`(缺省 20);`--json` 每条命中一行 JSON。
|
|
6
|
+
* - 输出一行一条:会话 id 前 8 位、条目时间、项目、角色与编号(user 是 `#n`,可直接给 `--from <id>#n`;
|
|
7
|
+
* 其它角色是条目序号 `@k`)、命中片段。stdout 是 TTY 且没设 NO_COLOR 时用 ANSI 高亮,否则纯文本。
|
|
8
|
+
*/
|
|
9
|
+
import { type SearchHit } from "../../session/search.js";
|
|
10
|
+
import type { CliIo } from "../deps.js";
|
|
11
|
+
export declare const SESSIONS_SEARCH_USAGE = "\u7528\u6CD5\uFF1Aama sessions search <\u5173\u952E\u8BCD|/\u6B63\u5219/\u6807\u5FD7> [--all] [--role user|assistant|tool]\n [--since 7d|today|YYYY-MM-DD] [--limit N] [--json] [--session-dir <\u76EE\u5F55>]\n";
|
|
12
|
+
export declare function renderHit(hit: SearchHit, color: boolean): string;
|
|
13
|
+
export declare function runSessionsSearch(argv: readonly string[], io: CliIo): Promise<number>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ama sessions search <关键词|/正则/>`:跨会话全文检索(只读)。[W4-D]
|
|
3
|
+
*
|
|
4
|
+
* - 范围:缺省当前目录的会话,`--all` 全部;`--role user|assistant|tool`(可逗号分隔多个);
|
|
5
|
+
* `--since 7d|today|YYYY-MM-DD`;`--limit N`(缺省 20);`--json` 每条命中一行 JSON。
|
|
6
|
+
* - 输出一行一条:会话 id 前 8 位、条目时间、项目、角色与编号(user 是 `#n`,可直接给 `--from <id>#n`;
|
|
7
|
+
* 其它角色是条目序号 `@k`)、命中片段。stdout 是 TTY 且没设 NO_COLOR 时用 ANSI 高亮,否则纯文本。
|
|
8
|
+
*/
|
|
9
|
+
import { join, resolve } from "node:path";
|
|
10
|
+
import { expandHome, resolveDataDir } from "../../config/paths.js";
|
|
11
|
+
import { sessionFilesInScope } from "../../session/scan.js";
|
|
12
|
+
import { compileQuery, searchSessions, } from "../../session/search.js";
|
|
13
|
+
import { parseSubArgs, UsageError } from "../args.js";
|
|
14
|
+
import { ExitCode } from "../exit-codes.js";
|
|
15
|
+
import { parseDaySpec } from "./stats.js";
|
|
16
|
+
export const SESSIONS_SEARCH_USAGE = `用法:ama sessions search <关键词|/正则/标志> [--all] [--role user|assistant|tool]
|
|
17
|
+
[--since 7d|today|YYYY-MM-DD] [--limit N] [--json] [--session-dir <目录>]
|
|
18
|
+
`;
|
|
19
|
+
const ROLES = ["user", "assistant", "tool"];
|
|
20
|
+
function highlight(text, ranges, color) {
|
|
21
|
+
if (!color || ranges.length === 0)
|
|
22
|
+
return text;
|
|
23
|
+
let out = "";
|
|
24
|
+
let at = 0;
|
|
25
|
+
for (const [a, b] of ranges) {
|
|
26
|
+
if (a < at)
|
|
27
|
+
continue;
|
|
28
|
+
out += text.slice(at, a) + `\x1b[1;33m${text.slice(a, b)}\x1b[0m`;
|
|
29
|
+
at = b;
|
|
30
|
+
}
|
|
31
|
+
return out + text.slice(at);
|
|
32
|
+
}
|
|
33
|
+
function shortTime(iso) {
|
|
34
|
+
return iso
|
|
35
|
+
.replace("T", " ")
|
|
36
|
+
.replace(/\.\d+Z$|Z$/, "")
|
|
37
|
+
.slice(0, 16);
|
|
38
|
+
}
|
|
39
|
+
export function renderHit(hit, color) {
|
|
40
|
+
const ref = hit.userN !== undefined ? `#${hit.userN}` : `@${hit.entryIndex}`;
|
|
41
|
+
const dim = (s) => (color ? `\x1b[2m${s}\x1b[0m` : s);
|
|
42
|
+
return `${hit.sessionId.slice(0, 8)}${ref.padEnd(4)} ${dim(shortTime(hit.timestamp))} ${dim(hit.cwd)} ${hit.role.padEnd(9)} ${highlight(hit.snippet, hit.ranges, color)}\n`;
|
|
43
|
+
}
|
|
44
|
+
export async function runSessionsSearch(argv, io) {
|
|
45
|
+
const { positionals, values, flags } = parseSubArgs(argv, ["role", "since", "limit", "session-dir"], ["all", "json"]);
|
|
46
|
+
if (flags.has("help")) {
|
|
47
|
+
io.stdout(SESSIONS_SEARCH_USAGE);
|
|
48
|
+
return ExitCode.Ok;
|
|
49
|
+
}
|
|
50
|
+
const pattern = positionals.join(" ");
|
|
51
|
+
if (pattern.trim() === "")
|
|
52
|
+
throw new UsageError("ama sessions search 需要关键词或 /正则/");
|
|
53
|
+
let query;
|
|
54
|
+
try {
|
|
55
|
+
query = compileQuery(pattern);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
throw new UsageError(`正则无效:${error.message}`);
|
|
59
|
+
}
|
|
60
|
+
let roles;
|
|
61
|
+
const roleRaw = values.get("role");
|
|
62
|
+
if (roleRaw !== undefined) {
|
|
63
|
+
roles = new Set();
|
|
64
|
+
for (const role of roleRaw.split(",").map((r) => r.trim())) {
|
|
65
|
+
if (!ROLES.includes(role)) {
|
|
66
|
+
throw new UsageError(`--role 的取值应为 ${ROLES.join(" | ")}(收到 ${role})`);
|
|
67
|
+
}
|
|
68
|
+
roles.add(role);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const limit = Number(values.get("limit") ?? "20");
|
|
72
|
+
if (!Number.isInteger(limit) || limit < 1)
|
|
73
|
+
throw new UsageError("--limit 应为正整数");
|
|
74
|
+
const sinceRaw = values.get("since");
|
|
75
|
+
const sinceIso = sinceRaw === undefined
|
|
76
|
+
? undefined
|
|
77
|
+
: new Date(`${parseDaySpec("since", sinceRaw)}T00:00:00`).toISOString();
|
|
78
|
+
const dirFlag = values.get("session-dir");
|
|
79
|
+
const root = dirFlag !== undefined
|
|
80
|
+
? resolve(io.cwd, expandHome(dirFlag, { env: io.env }))
|
|
81
|
+
: join(resolveDataDir({ env: io.env }), "sessions");
|
|
82
|
+
const files = sessionFilesInScope(root, flags.has("all") ? undefined : io.cwd);
|
|
83
|
+
const { hits, truncated } = searchSessions(files, query, {
|
|
84
|
+
limit,
|
|
85
|
+
...(roles !== undefined ? { roles } : {}),
|
|
86
|
+
...(sinceIso !== undefined ? { sinceIso } : {}),
|
|
87
|
+
});
|
|
88
|
+
if (flags.has("json")) {
|
|
89
|
+
for (const hit of hits)
|
|
90
|
+
io.stdout(`${JSON.stringify(hit)}\n`);
|
|
91
|
+
return ExitCode.Ok;
|
|
92
|
+
}
|
|
93
|
+
if (hits.length === 0) {
|
|
94
|
+
io.stdout(flags.has("all") ? "没有命中\n" : "没有命中(只搜了当前目录的会话,--all 搜全部)\n");
|
|
95
|
+
return ExitCode.Ok;
|
|
96
|
+
}
|
|
97
|
+
const color = io.stdoutIsTTY && (io.env["NO_COLOR"] ?? "") === "";
|
|
98
|
+
for (const hit of hits)
|
|
99
|
+
io.stdout(renderHit(hit, color));
|
|
100
|
+
if (truncated)
|
|
101
|
+
io.stdout(`(已到 --limit ${limit},可能还有更多)\n`);
|
|
102
|
+
return ExitCode.Ok;
|
|
103
|
+
}
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 会话存储是 B2 的实现,经 RuntimeDeps.sessions 注入;这里只做参数、目录解析与输出格式。
|
|
5
5
|
* - list [--all]:缺省只列当前目录的会话;`--all` 列全部。
|
|
6
|
-
* - show <id>:头信息 + 条目类型统计 +
|
|
6
|
+
* - show <id>:头信息 + 条目类型统计 + 首条提示 + 用户消息编号(`--from <id>#<编号>` 复用)。
|
|
7
|
+
* - search / export:见 sessions-search.ts、sessions-export.ts(只读扫描,不经会话存储)。
|
|
7
8
|
* - prune [--older-than <天>] [--dry-run]:缺省 30 天,移到 trash(不删除)。
|
|
8
9
|
*/
|
|
9
10
|
import type { CliIo, RuntimeDeps } from "../deps.js";
|
|
10
|
-
export declare const SESSIONS_USAGE = "\u7528\u6CD5\uFF1Aama sessions list [--all] [--session-dir <\u76EE\u5F55>]\n ama sessions show <id> [--session-dir <\u76EE\u5F55>]\n ama sessions prune [--older-than <\u5929>] [--dry-run] [--all] [--session-dir <\u76EE\u5F55>]\n";
|
|
11
|
+
export declare const SESSIONS_USAGE = "\u7528\u6CD5\uFF1Aama sessions list [--all] [--session-dir <\u76EE\u5F55>]\n ama sessions show <id> [--session-dir <\u76EE\u5F55>]\n ama sessions prune [--older-than <\u5929>] [--dry-run] [--all] [--session-dir <\u76EE\u5F55>]\n ama sessions search <\u5173\u952E\u8BCD|/\u6B63\u5219/> [--all] [--role user|assistant|tool] [--since 7d] [--limit N]\n ama sessions export <id> [--format md|json|jsonl] [--output <\u6587\u4EF6>] [--branch leaf|all]\n";
|
|
11
12
|
export declare function runSessions(argv: readonly string[], io: CliIo, deps: Pick<RuntimeDeps, "sessions"> | undefined): Promise<number>;
|
|
@@ -3,16 +3,22 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 会话存储是 B2 的实现,经 RuntimeDeps.sessions 注入;这里只做参数、目录解析与输出格式。
|
|
5
5
|
* - list [--all]:缺省只列当前目录的会话;`--all` 列全部。
|
|
6
|
-
* - show <id>:头信息 + 条目类型统计 +
|
|
6
|
+
* - show <id>:头信息 + 条目类型统计 + 首条提示 + 用户消息编号(`--from <id>#<编号>` 复用)。
|
|
7
|
+
* - search / export:见 sessions-search.ts、sessions-export.ts(只读扫描,不经会话存储)。
|
|
7
8
|
* - prune [--older-than <天>] [--dry-run]:缺省 30 天,移到 trash(不删除)。
|
|
8
9
|
*/
|
|
9
10
|
import { resolve } from "node:path";
|
|
10
11
|
import { expandHome, resolveDataDir } from "../../config/paths.js";
|
|
11
12
|
import { parseSubArgs, UsageError } from "../args.js";
|
|
12
13
|
import { ExitCode } from "../exit-codes.js";
|
|
14
|
+
import { numberUserMessages } from "../../session/reuse.js";
|
|
15
|
+
import { runSessionsExport } from "./sessions-export.js";
|
|
16
|
+
import { runSessionsSearch } from "./sessions-search.js";
|
|
13
17
|
export const SESSIONS_USAGE = `用法:ama sessions list [--all] [--session-dir <目录>]
|
|
14
18
|
ama sessions show <id> [--session-dir <目录>]
|
|
15
19
|
ama sessions prune [--older-than <天>] [--dry-run] [--all] [--session-dir <目录>]
|
|
20
|
+
ama sessions search <关键词|/正则/> [--all] [--role user|assistant|tool] [--since 7d] [--limit N]
|
|
21
|
+
ama sessions export <id> [--format md|json|jsonl] [--output <文件>] [--branch leaf|all]
|
|
16
22
|
`;
|
|
17
23
|
function shortTime(iso) {
|
|
18
24
|
return iso.replace("T", " ").replace(/\.\d+Z$|Z$/, "");
|
|
@@ -24,6 +30,10 @@ function oneLine(text, max = 60) {
|
|
|
24
30
|
return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
|
|
25
31
|
}
|
|
26
32
|
export async function runSessions(argv, io, deps) {
|
|
33
|
+
if (argv[0] === "search")
|
|
34
|
+
return runSessionsSearch(argv.slice(1), io);
|
|
35
|
+
if (argv[0] === "export")
|
|
36
|
+
return runSessionsExport(argv.slice(1), io);
|
|
27
37
|
const { positionals, values, flags } = parseSubArgs(argv, ["session-dir", "older-than"], ["all", "dry-run"]);
|
|
28
38
|
const action = positionals[0];
|
|
29
39
|
if (flags.has("help") || action === undefined) {
|
|
@@ -73,6 +83,17 @@ export async function runSessions(argv, io, deps) {
|
|
|
73
83
|
? [`首条提示:${oneLine(item.firstPrompt, 200)}`]
|
|
74
84
|
: []),
|
|
75
85
|
].join("\n") + "\n");
|
|
86
|
+
const users = numberUserMessages(entries);
|
|
87
|
+
if (users.length > 0) {
|
|
88
|
+
io.stdout(`用户消息(ama --from ${item.id.slice(0, 8)}#<编号> 复用):\n`);
|
|
89
|
+
for (const user of users) {
|
|
90
|
+
const tags = [
|
|
91
|
+
...(user.origin !== undefined ? [`[${user.origin}]`] : []),
|
|
92
|
+
...(user.images.length > 0 ? [`[图片 ${user.images.length}]`] : []),
|
|
93
|
+
];
|
|
94
|
+
io.stdout(` #${String(user.n).padEnd(3)} ${shortTime(user.timestamp)} ${[...tags, oneLine(user.text, 100)].join(" ")}\n`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
76
97
|
return ExitCode.Ok;
|
|
77
98
|
}
|
|
78
99
|
case "prune": {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ama stats`:跨会话的用量统计(只读扫描会话 JSONL)。[W4-D]
|
|
3
|
+
*
|
|
4
|
+
* - 范围:缺省当前目录的会话;`--project <目录>` 指定项目;`--all` 全部(`--by project` 未指定范围时也看全部)。
|
|
5
|
+
* - 时间:`--since` / `--until` 接受 `7d`(含今天的最近 7 天)、`today`、`YYYY-MM-DD`(本地日期,含当天)。
|
|
6
|
+
* - 分组:`--by day|week|month|provider|channel|model|project`。
|
|
7
|
+
* - 缓存:`<数据目录>/stats-index.json`,按文件 mtime / size 失效;`--no-cache` 不读也不写。
|
|
8
|
+
* - 口径见 session/stats-aggregate.ts 与 docs/sessions.md「统计」。
|
|
9
|
+
*/
|
|
10
|
+
import { type StatsReport } from "../../session/stats-aggregate.js";
|
|
11
|
+
import type { CliIo } from "../deps.js";
|
|
12
|
+
export declare const STATS_USAGE = "\u7528\u6CD5\uFF1Aama stats [--since 7d|30d|today|YYYY-MM-DD] [--until \u2026]\n [--by day|week|month|provider|channel|model|project]\n [--project <\u76EE\u5F55> | --all] [--top N] [--json] [--no-cache] [--session-dir <\u76EE\u5F55>]\n";
|
|
13
|
+
/** `7d` / `today` / `YYYY-MM-DD` → 本地日期。 */
|
|
14
|
+
export declare function parseDaySpec(option: string, raw: string, now?: Date): string;
|
|
15
|
+
export declare function formatTokens(count: number): string;
|
|
16
|
+
export declare function renderStats(report: StatsReport, scope: string): string;
|
|
17
|
+
export declare function runStats(argv: readonly string[], io: CliIo): Promise<number>;
|