cctra 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.
- package/LICENSE +21 -0
- package/README.md +135 -0
- package/bin/cctra +2 -0
- package/bin/cctra-daemon.exe +0 -0
- package/bin/cctra.js +2 -0
- package/examples/plugins/oauth-internal.js +46 -0
- package/examples/plugins/openai-compatible.js +27 -0
- package/package.json +53 -0
- package/src/canonical/types.ts +132 -0
- package/src/commands/add.ts +159 -0
- package/src/commands/daemon.ts +102 -0
- package/src/commands/ls.ts +49 -0
- package/src/commands/model.ts +95 -0
- package/src/commands/plugin.ts +167 -0
- package/src/commands/rename.ts +33 -0
- package/src/commands/rm.ts +37 -0
- package/src/commands/serve.ts +29 -0
- package/src/commands/shared.ts +14 -0
- package/src/commands/show.ts +46 -0
- package/src/commands/tier.ts +91 -0
- package/src/convert/common/content-blocks.ts +29 -0
- package/src/convert/common/extras.ts +38 -0
- package/src/convert/common/reasoning.ts +19 -0
- package/src/convert/common/system-prompt.ts +15 -0
- package/src/convert/common/tool-calls.ts +29 -0
- package/src/convert/common/usage.ts +19 -0
- package/src/convert/inbound/anthropic-to-canonical.ts +106 -0
- package/src/convert/inbound/chat-to-canonical.ts +132 -0
- package/src/convert/inbound/responses-to-canonical.ts +92 -0
- package/src/convert/outbound/canonical-to-anthropic.ts +62 -0
- package/src/convert/outbound/canonical-to-chat.ts +101 -0
- package/src/convert/outbound/canonical-to-responses.ts +105 -0
- package/src/convert/streaming/inbound/anthropic-stream.ts +14 -0
- package/src/convert/streaming/inbound/chat-stream.ts +219 -0
- package/src/convert/streaming/inbound/pick.ts +21 -0
- package/src/convert/streaming/inbound/responses-stream.ts +276 -0
- package/src/convert/streaming/outbound/format-anthropic.ts +19 -0
- package/src/convert/streaming/outbound/format-chat.ts +133 -0
- package/src/convert/streaming/outbound/format-responses.ts +184 -0
- package/src/convert/upstream/canonical-to-anthropic.ts +111 -0
- package/src/convert/upstream/canonical-to-chat.ts +115 -0
- package/src/convert/upstream/canonical-to-responses.ts +123 -0
- package/src/core/config.ts +156 -0
- package/src/core/model-fetch.ts +124 -0
- package/src/core/resolve.ts +73 -0
- package/src/core/routing.ts +31 -0
- package/src/core/source.ts +28 -0
- package/src/daemon/install.ts +47 -0
- package/src/daemon/platform/linux.ts +65 -0
- package/src/daemon/platform/macos.ts +71 -0
- package/src/daemon/platform/windows.ts +70 -0
- package/src/daemon/start.ts +22 -0
- package/src/daemon/status.ts +19 -0
- package/src/daemon/stop.ts +58 -0
- package/src/index.ts +34 -0
- package/src/plugin/contract.ts +51 -0
- package/src/plugin/host.ts +27 -0
- package/src/plugin/loader.ts +55 -0
- package/src/plugin/sandbox.ts +3 -0
- package/src/providers/presets.ts +167 -0
- package/src/server/anthropic-parser.ts +44 -0
- package/src/server/cancelable-fetch.ts +21 -0
- package/src/server/chat-parser.ts +81 -0
- package/src/server/error-status.ts +18 -0
- package/src/server/error.ts +16 -0
- package/src/server/handlers/chat-completions.ts +94 -0
- package/src/server/handlers/messages.ts +89 -0
- package/src/server/handlers/models.ts +35 -0
- package/src/server/handlers/responses.ts +89 -0
- package/src/server/keepalive.ts +63 -0
- package/src/server/responses-parser.ts +62 -0
- package/src/server/serve.ts +79 -0
- package/src/server/sse.ts +61 -0
- package/src/server/upstream.ts +251 -0
- package/src/tier/builtin.ts +9 -0
- package/src/tier/resolve.ts +33 -0
- package/src/tier/store.ts +3 -0
- package/src/types.ts +94 -0
- package/src/ui/format.ts +44 -0
- package/src/ui/prompts.ts +34 -0
- package/src/utils/fuzzy.ts +48 -0
- package/src/utils/logger.ts +32 -0
- package/src/utils/paths.ts +48 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// cctra ls:列出所有 source
|
|
3
|
+
// ============================================================================
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { withConfig } from "./shared";
|
|
6
|
+
import { green, dim, bold } from "../ui/format";
|
|
7
|
+
import { info } from "../ui/format";
|
|
8
|
+
|
|
9
|
+
export function registerLs(program: Command): void {
|
|
10
|
+
program
|
|
11
|
+
.command("ls")
|
|
12
|
+
.alias("list")
|
|
13
|
+
.description("List all subscriptions and plugins")
|
|
14
|
+
.action(() => {
|
|
15
|
+
withConfig((config) => {
|
|
16
|
+
const subs = Object.values(config.subscriptions);
|
|
17
|
+
const plugins = Object.values(config.plugins);
|
|
18
|
+
|
|
19
|
+
if (subs.length === 0 && plugins.length === 0) {
|
|
20
|
+
info("No subscriptions or plugins yet. Run `cctra add` to start.");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (subs.length > 0) {
|
|
25
|
+
console.log(bold("Subscriptions:"));
|
|
26
|
+
for (const [i, sub] of subs.entries()) {
|
|
27
|
+
const marker = green("*");
|
|
28
|
+
const index = dim(`${i + 1}.`);
|
|
29
|
+
const name = green(sub.name);
|
|
30
|
+
const vendorPart = sub.vendor ? ` ${dim(`[${sub.vendor}]`)}` : "";
|
|
31
|
+
const meta = ` ${dim(`(${sub.apiFormat}, ${sub.models.length} model${sub.models.length === 1 ? "" : "s"})`)}`;
|
|
32
|
+
console.log(`${marker} ${index} ${name}${vendorPart}${meta}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (plugins.length > 0) {
|
|
37
|
+
if (subs.length > 0) console.log("");
|
|
38
|
+
console.log(bold("Plugins:"));
|
|
39
|
+
for (const [i, p] of plugins.entries()) {
|
|
40
|
+
const marker = p.enabled ? green("*") : " ";
|
|
41
|
+
const index = dim(`${i + 1}.`);
|
|
42
|
+
const name = p.enabled ? p.name : dim(p.name);
|
|
43
|
+
const meta = ` ${dim(`(${p.enabled ? "enabled" : "disabled"}, ${p.models.length} model${p.models.length === 1 ? "" : "s"})`)}`;
|
|
44
|
+
console.log(`${marker} ${index} ${name}${meta}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// cctra model <subcommand>:管理订阅的模型
|
|
3
|
+
// add / ls / rm / rename
|
|
4
|
+
// ============================================================================
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import * as p from "@clack/prompts";
|
|
7
|
+
import { withConfig } from "./shared";
|
|
8
|
+
import { checkCancel } from "../ui/prompts";
|
|
9
|
+
import { success, error as errorOut } from "../ui/format";
|
|
10
|
+
|
|
11
|
+
export function registerModel(program: Command): void {
|
|
12
|
+
const model = program.command("model").description("Manage models in a subscription");
|
|
13
|
+
|
|
14
|
+
// cctra model add <sub>
|
|
15
|
+
model
|
|
16
|
+
.command("add <sub>")
|
|
17
|
+
.description("Add a model to a subscription")
|
|
18
|
+
.action(async (sub: string) => {
|
|
19
|
+
try {
|
|
20
|
+
const id = checkCancel(await p.text({ message: "Model ID (upstream):", validate: (v) => !v?.trim() ? "Required" : undefined }));
|
|
21
|
+
const aliasRaw = checkCancel(await p.text({ message: "Alias (optional):", defaultValue: "" }));
|
|
22
|
+
const alias = aliasRaw.trim() || undefined;
|
|
23
|
+
withConfig((config) => {
|
|
24
|
+
if (!config.subscriptions[sub]) throw new Error(`Subscription "${sub}" not found.`);
|
|
25
|
+
const s = config.subscriptions[sub]!;
|
|
26
|
+
if (s.models.find((m) => m.id === id)) throw new Error(`Model "${id}" already exists.`);
|
|
27
|
+
s.models.push({ id, alias });
|
|
28
|
+
s.updatedAt = Date.now();
|
|
29
|
+
});
|
|
30
|
+
success(`Added model "${id}".`);
|
|
31
|
+
} catch (e) {
|
|
32
|
+
errorOut((e as Error).message);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// cctra model ls <sub>
|
|
38
|
+
model
|
|
39
|
+
.command("ls <sub>")
|
|
40
|
+
.description("List models in a subscription")
|
|
41
|
+
.action((sub: string) => {
|
|
42
|
+
withConfig((config) => {
|
|
43
|
+
if (!config.subscriptions[sub]) {
|
|
44
|
+
errorOut(`Subscription "${sub}" not found.`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
for (const m of config.subscriptions[sub]!.models) {
|
|
48
|
+
const alias = m.alias ? ` (alias: ${m.alias})` : "";
|
|
49
|
+
console.log(`- ${m.id}${alias}`);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// cctra model rm <sub> <id-or-alias>
|
|
55
|
+
model
|
|
56
|
+
.command("rm <sub> <model>")
|
|
57
|
+
.description("Remove a model from a subscription")
|
|
58
|
+
.action((sub: string, model: string) => {
|
|
59
|
+
try {
|
|
60
|
+
withConfig((config) => {
|
|
61
|
+
if (!config.subscriptions[sub]) throw new Error(`Subscription "${sub}" not found.`);
|
|
62
|
+
const s = config.subscriptions[sub]!;
|
|
63
|
+
const idx = s.models.findIndex((m) => m.id === model || m.alias === model);
|
|
64
|
+
if (idx < 0) throw new Error(`Model "${model}" not found.`);
|
|
65
|
+
s.models.splice(idx, 1);
|
|
66
|
+
s.updatedAt = Date.now();
|
|
67
|
+
});
|
|
68
|
+
success(`Removed model "${model}".`);
|
|
69
|
+
} catch (e) {
|
|
70
|
+
errorOut((e as Error).message);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// cctra model rename <sub> <model> <new-alias>
|
|
76
|
+
model
|
|
77
|
+
.command("rename <sub> <model> <newAlias>")
|
|
78
|
+
.description("Set/change alias of a model")
|
|
79
|
+
.action((sub: string, model: string, newAlias: string) => {
|
|
80
|
+
try {
|
|
81
|
+
withConfig((config) => {
|
|
82
|
+
if (!config.subscriptions[sub]) throw new Error(`Subscription "${sub}" not found.`);
|
|
83
|
+
const s = config.subscriptions[sub]!;
|
|
84
|
+
const m = s.models.find((m) => m.id === model || m.alias === model);
|
|
85
|
+
if (!m) throw new Error(`Model "${model}" not found.`);
|
|
86
|
+
m.alias = newAlias.trim() || undefined;
|
|
87
|
+
s.updatedAt = Date.now();
|
|
88
|
+
});
|
|
89
|
+
success(`Updated alias.`);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
errorOut((e as Error).message);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// cctra plugin <subcommand>:管理插件
|
|
3
|
+
// add / ls / show / enable / disable / rm
|
|
4
|
+
// ============================================================================
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import * as p from "@clack/prompts";
|
|
7
|
+
import { existsSync, statSync, readFileSync } from "node:fs";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { withConfig } from "./shared";
|
|
10
|
+
import { checkCancel } from "../ui/prompts";
|
|
11
|
+
import { success, error as errorOut, warn, info, dim } from "../ui/format";
|
|
12
|
+
import { addPlugin, updatePlugin, removePlugin } from "../core/config";
|
|
13
|
+
import { loadPlugin, clearPluginCache } from "../plugin/loader";
|
|
14
|
+
import type { PluginConfig } from "../types";
|
|
15
|
+
|
|
16
|
+
export function registerPlugin(program: Command): void {
|
|
17
|
+
const plugin = program.command("plugin").description("Manage local-path plugins");
|
|
18
|
+
|
|
19
|
+
// cctra plugin add <name> <path>
|
|
20
|
+
plugin
|
|
21
|
+
.command("add <name> <path>")
|
|
22
|
+
.description("Add a local-path plugin")
|
|
23
|
+
.action(async (name: string, path: string) => {
|
|
24
|
+
try {
|
|
25
|
+
// 验证文件存在
|
|
26
|
+
if (!existsSync(path)) {
|
|
27
|
+
errorOut(`File not found: ${path}`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 计算 sha256
|
|
32
|
+
const content = readFileSync(path);
|
|
33
|
+
const sha256 = createHash("sha256").update(content).digest("hex");
|
|
34
|
+
const sizeKB = (statSync(path).size / 1024).toFixed(1);
|
|
35
|
+
|
|
36
|
+
// 警告 + 确认
|
|
37
|
+
warn("Plugin is arbitrary JavaScript. Loading gives it full process permissions.");
|
|
38
|
+
console.log(` ${dim("Path:")} ${path}`);
|
|
39
|
+
console.log(` ${dim("Size:")} ${sizeKB} KB`);
|
|
40
|
+
console.log(` ${dim("SHA-256:")} ${sha256}`);
|
|
41
|
+
const ok = checkCancel(
|
|
42
|
+
await p.confirm({ message: "Continue?", initialValue: false }),
|
|
43
|
+
);
|
|
44
|
+
if (!ok) return;
|
|
45
|
+
|
|
46
|
+
// 配置(这里先用空对象,v1 不做交互式 schema 推断)
|
|
47
|
+
const configRaw = checkCancel(
|
|
48
|
+
await p.text({ message: "Plugin config (JSON, optional):", defaultValue: "{}", validate: (v) => {
|
|
49
|
+
if (!v) return undefined;
|
|
50
|
+
try { JSON.parse(v); return undefined; } catch { return "Invalid JSON"; }
|
|
51
|
+
}}),
|
|
52
|
+
);
|
|
53
|
+
const config = JSON.parse(configRaw) as Record<string, unknown>;
|
|
54
|
+
|
|
55
|
+
// 尝试 import 一下确认合法
|
|
56
|
+
const tempPlugin: PluginConfig = {
|
|
57
|
+
kind: "plugin",
|
|
58
|
+
name,
|
|
59
|
+
path,
|
|
60
|
+
config,
|
|
61
|
+
enabled: true,
|
|
62
|
+
models: [],
|
|
63
|
+
};
|
|
64
|
+
const instance = await loadPlugin(tempPlugin, withConfig((c) => c));
|
|
65
|
+
if (!instance) {
|
|
66
|
+
errorOut(`Plugin failed to load. Check the file's syntax.`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
// 拉模型列表
|
|
70
|
+
let models: { id: string; alias?: string }[] = [];
|
|
71
|
+
if (instance.listModels) {
|
|
72
|
+
try {
|
|
73
|
+
const { makePluginContext } = await import("../plugin/host");
|
|
74
|
+
const ctx = makePluginContext(name, config);
|
|
75
|
+
models = (await instance.listModels(ctx)).map((m) => ({ id: m.id, alias: m.alias }));
|
|
76
|
+
} catch (e) {
|
|
77
|
+
warn(`listModels() failed: ${(e as Error).message}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
tempPlugin.models = models;
|
|
81
|
+
|
|
82
|
+
withConfig((c) => {
|
|
83
|
+
if (c.plugins[name]) updatePlugin(c, tempPlugin);
|
|
84
|
+
else addPlugin(c, tempPlugin);
|
|
85
|
+
});
|
|
86
|
+
success(`Added plugin "${name}" with ${models.length} model(s).`);
|
|
87
|
+
} catch (e) {
|
|
88
|
+
errorOut((e as Error).message);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// cctra plugin ls
|
|
94
|
+
plugin
|
|
95
|
+
.command("ls")
|
|
96
|
+
.description("List installed plugins")
|
|
97
|
+
.action(() => {
|
|
98
|
+
withConfig((config) => {
|
|
99
|
+
const plugins = Object.values(config.plugins);
|
|
100
|
+
if (plugins.length === 0) {
|
|
101
|
+
info("No plugins installed. Try `cctra plugin add <name> <path>`.");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
for (const p of plugins) {
|
|
105
|
+
const status = p.enabled ? "✓ enabled" : "✗ disabled";
|
|
106
|
+
console.log(`- ${p.name} ${dim(`(${p.models.length} model${p.models.length === 1 ? "" : "s"}, ${status})`)}`);
|
|
107
|
+
console.log(` ${dim("path:")} ${p.path}`);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// cctra plugin show <name>
|
|
113
|
+
plugin
|
|
114
|
+
.command("show <name>")
|
|
115
|
+
.description("Show plugin details")
|
|
116
|
+
.action((name: string) => {
|
|
117
|
+
withConfig((config) => {
|
|
118
|
+
const p = config.plugins[name];
|
|
119
|
+
if (!p) { errorOut(`Plugin "${name}" not found.`); return; }
|
|
120
|
+
console.log(`${p.name} ${dim(`(${p.enabled ? "enabled" : "disabled"})`)}`);
|
|
121
|
+
console.log(` ${dim("path:")} ${p.path}`);
|
|
122
|
+
console.log(` ${dim("config:")} ${JSON.stringify(p.config)}`);
|
|
123
|
+
console.log(` ${dim("models:")}`);
|
|
124
|
+
for (const m of p.models) {
|
|
125
|
+
const alias = m.alias ? ` (alias: ${m.alias})` : "";
|
|
126
|
+
console.log(` - ${m.id}${alias}`);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// cctra plugin enable / disable <name>
|
|
132
|
+
plugin
|
|
133
|
+
.command("enable <name>")
|
|
134
|
+
.description("Enable a plugin")
|
|
135
|
+
.action((name: string) => {
|
|
136
|
+
withConfig((config) => {
|
|
137
|
+
if (!config.plugins[name]) throw new Error(`Plugin "${name}" not found.`);
|
|
138
|
+
config.plugins[name]!.enabled = true;
|
|
139
|
+
});
|
|
140
|
+
clearPluginCache();
|
|
141
|
+
success(`Enabled "${name}".`);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
plugin
|
|
145
|
+
.command("disable <name>")
|
|
146
|
+
.description("Disable a plugin")
|
|
147
|
+
.action((name: string) => {
|
|
148
|
+
withConfig((config) => {
|
|
149
|
+
if (!config.plugins[name]) throw new Error(`Plugin "${name}" not found.`);
|
|
150
|
+
config.plugins[name]!.enabled = false;
|
|
151
|
+
});
|
|
152
|
+
clearPluginCache();
|
|
153
|
+
success(`Disabled "${name}".`);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// cctra plugin rm <name>
|
|
157
|
+
plugin
|
|
158
|
+
.command("rm <name>")
|
|
159
|
+
.description("Remove a plugin (does not delete the .js file)")
|
|
160
|
+
.action((name: string) => {
|
|
161
|
+
withConfig((config) => {
|
|
162
|
+
if (!config.plugins[name]) throw new Error(`Plugin "${name}" not found.`);
|
|
163
|
+
removePlugin(config, name);
|
|
164
|
+
});
|
|
165
|
+
success(`Removed plugin "${name}".`);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// cctra rename <old> <new>:重命名订阅
|
|
3
|
+
// ============================================================================
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { withConfig } from "./shared";
|
|
6
|
+
import { success, error as errorOut } from "../ui/format";
|
|
7
|
+
|
|
8
|
+
export function registerRename(program: Command): void {
|
|
9
|
+
program
|
|
10
|
+
.command("rename <old> <new>")
|
|
11
|
+
.description("Rename a subscription")
|
|
12
|
+
.action((oldName: string, newName: string) => {
|
|
13
|
+
try {
|
|
14
|
+
withConfig((config) => {
|
|
15
|
+
const normalized = newName.trim().toLowerCase();
|
|
16
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(normalized)) {
|
|
17
|
+
throw new Error('New name must be kebab-case: lowercase letters, digits, hyphens.');
|
|
18
|
+
}
|
|
19
|
+
if (!config.subscriptions[oldName]) throw new Error(`Subscription "${oldName}" not found.`);
|
|
20
|
+
if (config.subscriptions[normalized]) throw new Error(`"${normalized}" already exists.`);
|
|
21
|
+
const sub = config.subscriptions[oldName]!;
|
|
22
|
+
sub.name = normalized;
|
|
23
|
+
sub.updatedAt = Date.now();
|
|
24
|
+
config.subscriptions[normalized] = sub;
|
|
25
|
+
delete config.subscriptions[oldName];
|
|
26
|
+
});
|
|
27
|
+
success(`Renamed to "${newName}".`);
|
|
28
|
+
} catch (e) {
|
|
29
|
+
errorOut((e as Error).message);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// cctra rm <name>:删除订阅或插件
|
|
3
|
+
// ============================================================================
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import * as p from "@clack/prompts";
|
|
6
|
+
import { withConfig } from "./shared";
|
|
7
|
+
import { removeSubscription, removePlugin } from "../core/config";
|
|
8
|
+
import { checkCancel } from "../ui/prompts";
|
|
9
|
+
import { success, error as errorOut } from "../ui/format";
|
|
10
|
+
|
|
11
|
+
export function registerRm(program: Command): void {
|
|
12
|
+
program
|
|
13
|
+
.command("rm <name>")
|
|
14
|
+
.alias("remove")
|
|
15
|
+
.description("Remove a subscription or plugin")
|
|
16
|
+
.action(async (name: string) => {
|
|
17
|
+
try {
|
|
18
|
+
const ok = checkCancel(
|
|
19
|
+
await p.confirm({
|
|
20
|
+
message: `Delete "${name}"?`,
|
|
21
|
+
initialValue: false,
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
if (!ok) return;
|
|
25
|
+
|
|
26
|
+
withConfig((config) => {
|
|
27
|
+
if (config.subscriptions[name]) removeSubscription(config, name);
|
|
28
|
+
else if (config.plugins[name]) removePlugin(config, name);
|
|
29
|
+
else throw new Error(`Not found: "${name}"`);
|
|
30
|
+
});
|
|
31
|
+
success(`Removed "${name}".`);
|
|
32
|
+
} catch (e) {
|
|
33
|
+
errorOut((e as Error).message);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// cctra serve [--port N]:前台跑 HTTP server
|
|
3
|
+
// ============================================================================
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { startServer } from "../server/serve";
|
|
6
|
+
|
|
7
|
+
export function registerServe(program: Command): void {
|
|
8
|
+
program
|
|
9
|
+
.command("serve")
|
|
10
|
+
.description("Run the HTTP server in the foreground")
|
|
11
|
+
.option("-p, --port <port>", "Override port (default from config)")
|
|
12
|
+
.action(async (opts: { port?: string }) => {
|
|
13
|
+
const port = opts.port ? parseInt(opts.port, 10) : undefined;
|
|
14
|
+
// 启动信息由 server/serve.ts 里的 logger.info 负责(写 stderr + daemon.log),
|
|
15
|
+
// 不要再 console.log 一行 —— 终端会看到重复
|
|
16
|
+
const handle = startServer(port);
|
|
17
|
+
// 不退出
|
|
18
|
+
process.on("SIGINT", () => {
|
|
19
|
+
handle.stop();
|
|
20
|
+
process.exit(0);
|
|
21
|
+
});
|
|
22
|
+
process.on("SIGTERM", () => {
|
|
23
|
+
handle.stop();
|
|
24
|
+
process.exit(0);
|
|
25
|
+
});
|
|
26
|
+
// 永久 hang
|
|
27
|
+
await new Promise(() => {});
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// 共享工具:读/写 config 的 helper
|
|
3
|
+
// ============================================================================
|
|
4
|
+
import { loadConfigFile, saveConfigFile } from "../core/config";
|
|
5
|
+
import { ensureCctraDir } from "../utils/paths";
|
|
6
|
+
import type { Config } from "../types";
|
|
7
|
+
|
|
8
|
+
export function withConfig<T>(fn: (config: Config) => T): T {
|
|
9
|
+
ensureCctraDir();
|
|
10
|
+
const config = loadConfigFile();
|
|
11
|
+
const result = fn(config);
|
|
12
|
+
saveConfigFile(config);
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// cctra show <name>:显示订阅/插件详情
|
|
3
|
+
// ============================================================================
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { withConfig } from "./shared";
|
|
6
|
+
import { maskToken } from "../ui/prompts";
|
|
7
|
+
import { bold, dim, info } from "../ui/format";
|
|
8
|
+
import { getSource, isSubscription, isPlugin } from "../core/source";
|
|
9
|
+
|
|
10
|
+
export function registerShow(program: Command): void {
|
|
11
|
+
program
|
|
12
|
+
.command("show <name>")
|
|
13
|
+
.description("Show details of a subscription or plugin")
|
|
14
|
+
.action((name: string) => {
|
|
15
|
+
withConfig((config) => {
|
|
16
|
+
const s = getSource(config, name);
|
|
17
|
+
if (!s) {
|
|
18
|
+
info(`Not found: "${name}"`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log(bold(`${s.name}`) + ` ${dim(`(${s.kind})`)}`);
|
|
23
|
+
if (isSubscription(s)) {
|
|
24
|
+
if (s.vendor) {
|
|
25
|
+
console.log(` ${dim("vendor:")} ${s.vendor}`);
|
|
26
|
+
}
|
|
27
|
+
console.log(` ${dim("endpoint:")} ${s.endpoint}`);
|
|
28
|
+
console.log(` ${dim("token:")} ${maskToken(s.token)}`);
|
|
29
|
+
console.log(` ${dim("format:")} ${s.apiFormat}`);
|
|
30
|
+
} else if (isPlugin(s)) {
|
|
31
|
+
console.log(` ${dim("path:")} ${s.path}`);
|
|
32
|
+
console.log(` ${dim("enabled:")} ${s.enabled}`);
|
|
33
|
+
console.log(` ${dim("config:")} ${JSON.stringify(s.config)}`);
|
|
34
|
+
}
|
|
35
|
+
console.log(` ${dim("models:")}`);
|
|
36
|
+
if (s.models.length === 0) {
|
|
37
|
+
console.log(` ${dim("(none)")}`);
|
|
38
|
+
} else {
|
|
39
|
+
for (const m of s.models) {
|
|
40
|
+
const alias = m.alias ? ` ${dim(`(alias: ${m.alias})`)}` : "";
|
|
41
|
+
console.log(` - ${m.id}${alias}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// cctra tier <subcommand>:管理 tier 映射
|
|
3
|
+
// set / ls / show / rm
|
|
4
|
+
// ============================================================================
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { withConfig } from "./shared";
|
|
7
|
+
import { success, error as errorOut, info, dim, bold } from "../ui/format";
|
|
8
|
+
import { setTier, removeTier } from "../core/config";
|
|
9
|
+
import { BUILTIN_TIERS } from "../types";
|
|
10
|
+
|
|
11
|
+
export function registerTier(program: Command): void {
|
|
12
|
+
const tier = program.command("tier").description("Manage tier model mappings");
|
|
13
|
+
|
|
14
|
+
// cctra tier set <name> <target>
|
|
15
|
+
tier
|
|
16
|
+
.command("set <name> <target>")
|
|
17
|
+
.description("Set/update a tier mapping (e.g. `cctra tier set cctra-pro my-sub/model-x`)")
|
|
18
|
+
.action((name: string, target: string) => {
|
|
19
|
+
try {
|
|
20
|
+
withConfig((config) => {
|
|
21
|
+
setTier(config, { name, target, description: config.tiers[name]?.description });
|
|
22
|
+
});
|
|
23
|
+
success(`Mapped ${name} → ${target}.`);
|
|
24
|
+
} catch (e) {
|
|
25
|
+
errorOut((e as Error).message);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// cctra tier ls
|
|
31
|
+
tier
|
|
32
|
+
.command("ls")
|
|
33
|
+
.description("List all tier mappings")
|
|
34
|
+
.action(() => {
|
|
35
|
+
withConfig((config) => {
|
|
36
|
+
const all = Object.values(config.tiers);
|
|
37
|
+
const builtinSet = new Set<string>(BUILTIN_TIERS);
|
|
38
|
+
const builtin = all.filter((t) => builtinSet.has(t.name));
|
|
39
|
+
const custom = all.filter((t) => !builtinSet.has(t.name));
|
|
40
|
+
|
|
41
|
+
console.log(bold("Built-in tiers:"));
|
|
42
|
+
for (const t of builtin) {
|
|
43
|
+
const target = t.target || dim("(not mapped)");
|
|
44
|
+
console.log(` ${t.name.padEnd(15)} → ${target} ${dim(t.description ?? "")}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (custom.length > 0) {
|
|
48
|
+
console.log("");
|
|
49
|
+
console.log(bold("Custom tiers:"));
|
|
50
|
+
for (const t of custom) {
|
|
51
|
+
const target = t.target || dim("(not mapped)");
|
|
52
|
+
console.log(` ${t.name.padEnd(15)} → ${target} ${dim(t.description ?? "")}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (builtin.length + custom.length === 0) {
|
|
57
|
+
info("No tiers configured.");
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// cctra tier show <name>
|
|
63
|
+
tier
|
|
64
|
+
.command("show <name>")
|
|
65
|
+
.description("Show a tier's current target")
|
|
66
|
+
.action((name: string) => {
|
|
67
|
+
withConfig((config) => {
|
|
68
|
+
const t = config.tiers[name];
|
|
69
|
+
if (!t) { errorOut(`Tier "${name}" not found.`); return; }
|
|
70
|
+
const target = t.target || dim("(not mapped)");
|
|
71
|
+
console.log(`${name} → ${target}`);
|
|
72
|
+
if (t.description) console.log(dim(t.description));
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// cctra tier rm <name>
|
|
77
|
+
tier
|
|
78
|
+
.command("rm <name>")
|
|
79
|
+
.description("Unmap a tier (built-ins keep the name, just clear target)")
|
|
80
|
+
.action((name: string) => {
|
|
81
|
+
try {
|
|
82
|
+
withConfig((config) => {
|
|
83
|
+
removeTier(config, name);
|
|
84
|
+
});
|
|
85
|
+
success(`Unmapped ${name}.`);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
errorOut((e as Error).message);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { CanonicalContentBlock } from "../../canonical/types";
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Content Block 工具函数
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
/** 把字符串或 block 数组统一成 block 数组(便于遍历) */
|
|
8
|
+
export function ensureBlocks(content: string | CanonicalContentBlock[]): CanonicalContentBlock[] {
|
|
9
|
+
if (typeof content === "string") return [{ type: "text", text: content }];
|
|
10
|
+
return content;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** 提取所有文本块(用于 Anthropic 风格的 system prompt 简化) */
|
|
14
|
+
export function extractText(blocks: CanonicalContentBlock[]): string {
|
|
15
|
+
return blocks
|
|
16
|
+
.filter((b): b is { type: "text"; text: string } => b.type === "text")
|
|
17
|
+
.map((b) => b.text)
|
|
18
|
+
.join("");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 判断 block 是否是 tool_use */
|
|
22
|
+
export function isToolUse(b: CanonicalContentBlock): b is { type: "tool_use"; id: string; name: string; input: unknown } {
|
|
23
|
+
return b.type === "tool_use";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 判断 block 是否是 tool_result */
|
|
27
|
+
export function isToolResult(b: CanonicalContentBlock): b is { type: "tool_result"; toolUseId: string; content: string | CanonicalContentBlock[]; isError?: boolean } {
|
|
28
|
+
return b.type === "tool_result";
|
|
29
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Per-protocol extras helpers
|
|
3
|
+
// 在 inbound 时把已知字段剥离,剩余未识别字段塞进对应协议桶
|
|
4
|
+
// 在 outbound 时按目标协议把桶 spread 回对象
|
|
5
|
+
// ============================================================================
|
|
6
|
+
import type { ProtocolExtras } from "../../canonical/types";
|
|
7
|
+
|
|
8
|
+
/** 把 obj 中 knownKeys 之外的字段剥出来,组成对应协议的 extras 桶 */
|
|
9
|
+
export function splitKnownAndExtras<T extends Record<string, unknown>>(
|
|
10
|
+
obj: T,
|
|
11
|
+
knownKeys: ReadonlySet<keyof T>,
|
|
12
|
+
protocol: keyof ProtocolExtras,
|
|
13
|
+
): { known: T; extras: ProtocolExtras } {
|
|
14
|
+
const known = {} as T;
|
|
15
|
+
const extrasBag: Record<string, unknown> = {};
|
|
16
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
17
|
+
if (knownKeys.has(k as keyof T)) {
|
|
18
|
+
(known as Record<string, unknown>)[k] = v;
|
|
19
|
+
} else {
|
|
20
|
+
extrasBag[k] = v;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const extras: ProtocolExtras = {};
|
|
24
|
+
if (Object.keys(extrasBag).length > 0) {
|
|
25
|
+
extras[protocol] = extrasBag;
|
|
26
|
+
}
|
|
27
|
+
return { known, extras };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 把 extras 中目标协议桶的字段 spread 进 target;防御性:target 已知字段优先 */
|
|
31
|
+
export function mergeExtras<T extends Record<string, unknown>>(
|
|
32
|
+
target: T,
|
|
33
|
+
extras: ProtocolExtras | undefined,
|
|
34
|
+
protocol: keyof ProtocolExtras,
|
|
35
|
+
): T {
|
|
36
|
+
if (!extras?.[protocol]) return target;
|
|
37
|
+
return { ...target, ...extras[protocol] } as T;
|
|
38
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CanonicalContentBlock } from "../../canonical/types";
|
|
2
|
+
|
|
3
|
+
// 处理 reasoning/thinking 块的辅助
|
|
4
|
+
// Anthropic 的 thinking block 有 signature,cc-switch 的 thinking_rectifier 模式下
|
|
5
|
+
// 我们只在 signature 存在时回传,避免浪费 token
|
|
6
|
+
|
|
7
|
+
export function stripThinkingContent(blocks: CanonicalContentBlock[]): CanonicalContentBlock[] {
|
|
8
|
+
return blocks.map((b) => {
|
|
9
|
+
if (b.type === "thinking") {
|
|
10
|
+
// 只保留 signature(如果有),去掉 thinking 内容
|
|
11
|
+
return { type: "thinking", thinking: "", signature: b.signature };
|
|
12
|
+
}
|
|
13
|
+
return b;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function hasThinkingSignature(blocks: CanonicalContentBlock[]): boolean {
|
|
18
|
+
return blocks.some((b) => b.type === "thinking" && b.signature);
|
|
19
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CanonicalRequest, CanonicalContentBlock } from "../../canonical/types";
|
|
2
|
+
import { ensureBlocks, extractText } from "./content-blocks";
|
|
3
|
+
|
|
4
|
+
/** 把 Canonical 顶级 system 转成字符串(用于 OpenAI Chat 的 messages[0].role=system) */
|
|
5
|
+
export function systemToString(system: CanonicalRequest["system"]): string | undefined {
|
|
6
|
+
if (system === undefined) return undefined;
|
|
7
|
+
if (typeof system === "string") return system;
|
|
8
|
+
return extractText(ensureBlocks(system));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** 把字符串 system 转成 Canonical 顶级 system(block 形式) */
|
|
12
|
+
export function stringToSystem(s: string | undefined): CanonicalContentBlock[] | undefined {
|
|
13
|
+
if (!s) return undefined;
|
|
14
|
+
return [{ type: "text", text: s }];
|
|
15
|
+
}
|