@yijiancaotang/cli 1.0.0 → 1.1.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/README.md +41 -9
- package/dist/auth.js +7 -4
- package/dist/calendar-contract.js +358 -0
- package/dist/calendar-help.js +12 -0
- package/dist/calendar.js +64 -0
- package/dist/capability-result.js +5 -0
- package/dist/cli.js +12 -3
- package/dist/command-help.js +10 -5
- package/dist/commands.js +6 -0
- package/dist/doctor.js +11 -4
- package/dist/habit-read.js +3 -1
- package/dist/habit-wire.js +36 -5
- package/dist/http.js +11 -1
- package/dist/installation.js +307 -37
- package/dist/local-commands.js +107 -14
- package/dist/local-records.js +149 -0
- package/dist/managed-skill-baseline.js +35 -0
- package/dist/missing-sections.js +1 -0
- package/dist/output.js +3 -1
- package/dist/platforms.js +51 -12
- package/dist/query-search.js +3 -2
- package/dist/reviewed-managed-bundle-baseline.js +356 -0
- package/dist/schedule-contract.js +289 -0
- package/dist/schedule-help.js +35 -0
- package/dist/schedule-start-wire.js +80 -0
- package/dist/schedule.js +70 -0
- package/dist/skill-assets.js +1 -1
- package/dist/web-citations.js +1 -1
- package/dist/write-routing.js +26 -0
- package/package.json +1 -1
- package/skills/caotang-context/SKILL.md +5 -0
- package/skills/caotang-first-connect/SKILL.md +18 -6
- package/skills/caotang-project-progress/SKILL.md +6 -2
- package/skills/caotang-save-knowledge/SKILL.md +6 -4
- package/skills/caotang-schedule/SKILL.md +54 -0
- package/skills/caotang-search/SKILL.md +11 -0
- package/skills/caotang-workday/SKILL.md +56 -9
- package/skills/manifest.json +28 -6
package/dist/installation.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { constants, lstatSync, fstatSync, openSync, closeSync, readSync, writeFileSync, fsyncSync, fchmodSync, mkdirSync, readdirSync, rmdirSync } from "node:fs";
|
|
1
|
+
import { constants, lstatSync, fstatSync, openSync, closeSync, readSync, writeFileSync, fsyncSync, fchmodSync, ftruncateSync, mkdirSync, readdirSync, rmdirSync } from "node:fs";
|
|
2
2
|
import { access, lstat, stat, realpath, open, readdir } from "node:fs/promises";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
@@ -7,6 +7,8 @@ import { CliError, asRecord } from "./errors.js";
|
|
|
7
7
|
import { PLATFORM_IDS, isPlatformId, platformSupported } from "./local-types.js";
|
|
8
8
|
import { bindSkillAgent, completeSkillInventory } from "./skill-assets.js";
|
|
9
9
|
import { acquireLocalLock } from "./local-lock.js";
|
|
10
|
+
import { LEGACY_MANAGED_SKILLS } from "./managed-skill-baseline.js";
|
|
11
|
+
import { REVIEWED_MANAGED_BUNDLE_1_1_0 } from "./reviewed-managed-bundle-baseline.js";
|
|
10
12
|
const same = (a, b) => a.dev === b.dev && a.ino === b.ino;
|
|
11
13
|
const missing = (error) => asRecord(error)?.code === "ENOENT";
|
|
12
14
|
const unsafe = () => new CliError("installation_unsafe", "本地安装位置未通过安全检查。", "请人工检查属主、权限、符号链接与目标目录;不要自动删除、覆盖或修改凭据。");
|
|
@@ -128,8 +130,8 @@ export class OwnedTree {
|
|
|
128
130
|
if (!info.isFile() || info.uid !== this.uid || info.nlink !== 1 || (info.mode & 0o022) !== 0 || info.size > max)
|
|
129
131
|
throw unsafe();
|
|
130
132
|
}
|
|
131
|
-
readFile(name, max, executable = false) {
|
|
132
|
-
const check = (info) => { this.checkFile(info, max); if (executable && (info.mode & 0o100) === 0)
|
|
133
|
+
readFile(name, max, executable = false, mode) {
|
|
134
|
+
const check = (info) => { this.checkFile(info, max); if (executable && (info.mode & 0o100) === 0 || mode !== undefined && (info.mode & 0o7777) !== mode)
|
|
133
135
|
throw unsafe(); };
|
|
134
136
|
let before;
|
|
135
137
|
try {
|
|
@@ -158,9 +160,9 @@ export class OwnedTree {
|
|
|
158
160
|
closeSync(fd);
|
|
159
161
|
}
|
|
160
162
|
}
|
|
161
|
-
async read(path, max = 1048576) {
|
|
163
|
+
async read(path, max = 1048576, mode) {
|
|
162
164
|
return this.directory(dirname(path), false, async (directories) => {
|
|
163
|
-
return pinned(directories.at(-1), () => this.readFile(basename(path), max));
|
|
165
|
+
return pinned(directories.at(-1), () => this.readFile(basename(path), max, false, mode));
|
|
164
166
|
});
|
|
165
167
|
}
|
|
166
168
|
async list(path) {
|
|
@@ -237,6 +239,43 @@ export class OwnedTree {
|
|
|
237
239
|
});
|
|
238
240
|
return result === true;
|
|
239
241
|
}
|
|
242
|
+
// Never truncate on open. Only an exact, single-link managed inode may change;
|
|
243
|
+
// the caller durably preserves its public before/after bytes before calling.
|
|
244
|
+
async replaceManaged(entry, rollback = false) {
|
|
245
|
+
const result = await this.directory(dirname(entry.path), false, async (directories) => pinned(directories.at(-1), () => {
|
|
246
|
+
const name = basename(entry.path), before = lstatSync(name);
|
|
247
|
+
const check = (info) => { this.checkFile(info, 1048576); if ((info.mode & 0o7777) !== entry.mode)
|
|
248
|
+
throw unsafe(); };
|
|
249
|
+
check(before);
|
|
250
|
+
const fd = openSync(name, constants.O_RDWR | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
251
|
+
try {
|
|
252
|
+
const opened = fstatSync(fd);
|
|
253
|
+
check(opened);
|
|
254
|
+
if (!unchanged(before, opened) || rollback && (!entry.identity || !same(entry.identity, opened)))
|
|
255
|
+
throw unsafe();
|
|
256
|
+
const expected = rollback ? entry.after : entry.before, next = rollback ? entry.before : entry.after;
|
|
257
|
+
if (!readDescriptor(fd, 1048576).equals(expected) || !unchanged(opened, fstatSync(fd)) || !unchanged(opened, lstatSync(name)))
|
|
258
|
+
throw unsafe();
|
|
259
|
+
if (!rollback)
|
|
260
|
+
entry.identity = opened;
|
|
261
|
+
writeFileSync(fd, next);
|
|
262
|
+
ftruncateSync(fd, next.length);
|
|
263
|
+
fsyncSync(fd);
|
|
264
|
+
const written = fstatSync(fd), named = lstatSync(name);
|
|
265
|
+
check(written);
|
|
266
|
+
check(named);
|
|
267
|
+
if (!same(opened, written) || !unchanged(written, named) || written.size !== next.length || !readDescriptor(fd, 1048576).equals(next)
|
|
268
|
+
|| !unchanged(written, fstatSync(fd)) || !unchanged(written, lstatSync(name)))
|
|
269
|
+
throw unsafe();
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
finally {
|
|
273
|
+
closeSync(fd);
|
|
274
|
+
}
|
|
275
|
+
}));
|
|
276
|
+
if (result !== true)
|
|
277
|
+
throw unsafe();
|
|
278
|
+
}
|
|
240
279
|
async removeDirectory(path, expected) {
|
|
241
280
|
await this.directory(dirname(path), false, async (directories) => {
|
|
242
281
|
pinned(directories.at(-1), () => {
|
|
@@ -281,6 +320,9 @@ async function readSource(path, max = 1048576) {
|
|
|
281
320
|
throw sourceInvalid();
|
|
282
321
|
}
|
|
283
322
|
}
|
|
323
|
+
function managedShim(directory) {
|
|
324
|
+
return Buffer.from(`#!/usr/bin/env node\n// caotang managed command v1\nimport(${JSON.stringify(pathToFileURL(join(directory, "bin", "caotang.cjs")).href)}).catch(()=>{process.stderr.write(JSON.stringify({code:"cli_unavailable",message:"本地命令行暂不可用。",next_action:"请人工核对本地安装,不要重复业务写入。"})+"\\n");process.exitCode=7;});\n`);
|
|
325
|
+
}
|
|
284
326
|
async function bundle(ctx, tree) {
|
|
285
327
|
if (!completeSkillInventory(ctx.assets.map(asset => asset.name)) || ctx.assets.some(asset => hash(asset.content) !== asset.sha256 || !asset.content || Buffer.byteLength(asset.content) > 65536))
|
|
286
328
|
throw sourceInvalid();
|
|
@@ -316,7 +358,7 @@ async function bundle(ctx, tree) {
|
|
|
316
358
|
for (const file of files)
|
|
317
359
|
digest.update(`${file.path}\0${file.mode}\0${file.bytes.length}\0`).update(file.bytes);
|
|
318
360
|
const sha = digest.digest("hex"), directory = join(tree.home, ".caotang", "cli", "releases", sha), command = join(tree.home, ".local", "bin", "caotang");
|
|
319
|
-
const shim =
|
|
361
|
+
const shim = managedShim(directory);
|
|
320
362
|
return { files, sha, directory, command, shim, version: info.version };
|
|
321
363
|
}
|
|
322
364
|
function validateTargets(ctx, tree) {
|
|
@@ -378,25 +420,160 @@ async function onPath(command, env) {
|
|
|
378
420
|
}
|
|
379
421
|
return false;
|
|
380
422
|
}
|
|
423
|
+
// The fixed 1.1.0 baseline is admitted only after its complete public runtime
|
|
424
|
+
// inventory verifies. Never infer provenance from a version or receipt.
|
|
425
|
+
async function reviewedV110Bundle(tree, value) {
|
|
426
|
+
const pin = REVIEWED_MANAGED_BUNDLE_1_1_0, directory = join(tree.home, ".caotang", "cli", "releases", pin.sha);
|
|
427
|
+
const children = new Map([["", new Set()]]);
|
|
428
|
+
for (const file of pin.files) {
|
|
429
|
+
const parts = file.path.split("/");
|
|
430
|
+
let parent = "";
|
|
431
|
+
for (const [index, part] of parts.entries()) {
|
|
432
|
+
children.get(parent).add(part);
|
|
433
|
+
if (index < parts.length - 1) {
|
|
434
|
+
parent = parent ? `${parent}/${part}` : part;
|
|
435
|
+
if (!children.has(parent))
|
|
436
|
+
children.set(parent, new Set());
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
const verifyInventory = async () => {
|
|
441
|
+
for (const [path, expected] of children)
|
|
442
|
+
if ((await tree.list(join(directory, path)))?.join(",") !== [...expected].sort().join(","))
|
|
443
|
+
throw conflict();
|
|
444
|
+
};
|
|
445
|
+
await verifyInventory();
|
|
446
|
+
const files = [], digest = createHash("sha256");
|
|
447
|
+
for (const file of pin.files) {
|
|
448
|
+
const bytes = await tree.read(join(directory, file.path), file.bytes, file.mode);
|
|
449
|
+
if (!bytes || bytes.length !== file.bytes || hash(bytes) !== file.sha256)
|
|
450
|
+
throw conflict();
|
|
451
|
+
files.push({ path: file.path, bytes, mode: file.mode });
|
|
452
|
+
digest.update(`${file.path}\0${file.mode}\0${bytes.length}\0`).update(bytes);
|
|
453
|
+
}
|
|
454
|
+
if (digest.digest("hex") !== pin.sha)
|
|
455
|
+
throw conflict();
|
|
456
|
+
await verifyInventory();
|
|
457
|
+
const manifest = JSON.parse(files.find(file => file.path === "skills/manifest.json").bytes.toString("utf8"));
|
|
458
|
+
if (!completeSkillInventory(manifest.skills.map(item => item.name)))
|
|
459
|
+
throw conflict();
|
|
460
|
+
const assets = manifest.skills.map(item => { const bytes = files.find(file => file.path === `skills/${item.name}/SKILL.md`).bytes; return { ...item, content: bytes.toString("utf8"), sha256: hash(bytes) }; });
|
|
461
|
+
return { bundle: { files, sha: pin.sha, version: pin.version, directory, command: value.command, shim: managedShim(directory) }, assets };
|
|
462
|
+
}
|
|
463
|
+
// Keep the reviewed 1.0.0 six-skill baseline unchanged. A directory digest alone
|
|
464
|
+
// cannot authorize replacing somebody's same-name skill.
|
|
465
|
+
async function legacyBundles(ctx, tree, value) {
|
|
466
|
+
if (!["1.0.1", "1.1.0"].includes(value.version) || ctx.agentName === undefined)
|
|
467
|
+
return [];
|
|
468
|
+
const releases = join(tree.home, ".caotang", "cli", "releases"), entries = await tree.list(releases) ?? [];
|
|
469
|
+
if (entries.length > 128)
|
|
470
|
+
throw unsafe();
|
|
471
|
+
const found = [];
|
|
472
|
+
const runtime = Buffer.from(JSON.stringify({ name: "@yijiancaotang/cli", version: "1.0.0", private: true, type: "module", engines: { node: ">=20" }, bin: { caotang: "bin/caotang.cjs" } }));
|
|
473
|
+
const manifest = Buffer.from(JSON.stringify({ formatVersion: 1, skills: LEGACY_MANAGED_SKILLS.map(({ name, description }) => ({ name, description })) }));
|
|
474
|
+
for (const sha of entries) {
|
|
475
|
+
if (!/^[a-f0-9]{64}$/.test(sha) || sha === value.sha)
|
|
476
|
+
continue;
|
|
477
|
+
const directory = join(releases, sha), packageBytes = await tree.read(join(directory, "package.json"), 32768, 0o600);
|
|
478
|
+
if (!packageBytes?.equals(runtime))
|
|
479
|
+
continue;
|
|
480
|
+
const root = await tree.list(directory), bin = await tree.list(join(directory, "bin")), dist = await tree.list(join(directory, "dist"));
|
|
481
|
+
const skills = await tree.list(join(directory, "skills")), generated = await tree.list(join(directory, "dist/generated"));
|
|
482
|
+
if (root?.join(",") !== "bin,dist,package.json,skills" || bin?.join(",") !== "caotang.cjs" || generated?.join(",") !== "knowledge-growth.js"
|
|
483
|
+
|| !dist || dist.length > 64 || !dist.includes("cli.js") || !dist.includes("generated") || dist.some(name => name !== "generated" && !/^[a-z][a-z0-9-]*\.js$/.test(name))
|
|
484
|
+
|| skills?.join(",") !== [...LEGACY_MANAGED_SKILLS.map(item => item.name), "manifest.json"].sort().join(","))
|
|
485
|
+
throw conflict();
|
|
486
|
+
if (!(await tree.read(join(directory, "skills/manifest.json"), 16384, 0o600))?.equals(manifest))
|
|
487
|
+
throw conflict();
|
|
488
|
+
const files = [{ path: "package.json", bytes: runtime, mode: 0o600 }, { path: "skills/manifest.json", bytes: manifest, mode: 0o600 }];
|
|
489
|
+
const assets = [];
|
|
490
|
+
for (const item of LEGACY_MANAGED_SKILLS) {
|
|
491
|
+
if ((await tree.list(join(directory, "skills", item.name)))?.join(",") !== "SKILL.md")
|
|
492
|
+
throw conflict();
|
|
493
|
+
const bytes = await tree.read(join(directory, "skills", item.name, "SKILL.md"), 65536, 0o600);
|
|
494
|
+
if (!bytes || hash(bytes) !== item.sha256)
|
|
495
|
+
throw conflict();
|
|
496
|
+
assets.push({ ...item, content: bytes.toString("utf8") });
|
|
497
|
+
files.push({ path: `skills/${item.name}/SKILL.md`, bytes, mode: 0o600 });
|
|
498
|
+
}
|
|
499
|
+
for (const path of ["bin/caotang.cjs", ...dist.filter(name => name !== "generated").map(name => `dist/${name}`), "dist/generated/knowledge-growth.js"]) {
|
|
500
|
+
const mode = path === "bin/caotang.cjs" ? 0o700 : 0o600, bytes = await tree.read(join(directory, path), 1048576, mode);
|
|
501
|
+
if (!bytes?.length)
|
|
502
|
+
throw conflict();
|
|
503
|
+
files.push({ path, bytes, mode });
|
|
504
|
+
}
|
|
505
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
506
|
+
if (files.reduce((total, file) => total + file.bytes.length, 0) > 5242880)
|
|
507
|
+
throw conflict();
|
|
508
|
+
const digest = createHash("sha256");
|
|
509
|
+
for (const file of files)
|
|
510
|
+
digest.update(`${file.path}\0${file.mode}\0${file.bytes.length}\0`).update(file.bytes);
|
|
511
|
+
if (digest.digest("hex") !== sha)
|
|
512
|
+
throw conflict();
|
|
513
|
+
found.push({ bundle: { files, sha, version: "1.0.0", directory, command: value.command, shim: managedShim(directory) }, assets });
|
|
514
|
+
}
|
|
515
|
+
if (value.version === "1.1.0" && value.sha !== REVIEWED_MANAGED_BUNDLE_1_1_0.sha && entries.includes(REVIEWED_MANAGED_BUNDLE_1_1_0.sha))
|
|
516
|
+
found.push(await reviewedV110Bundle(tree, value));
|
|
517
|
+
return found;
|
|
518
|
+
}
|
|
519
|
+
async function installPlan(ctx, tree, value) {
|
|
520
|
+
const missing = [], modified = [];
|
|
521
|
+
const newlyInstalledPlatforms = [], upgradedPlatforms = [], replacements = [];
|
|
522
|
+
for (const target of ctx.targets) {
|
|
523
|
+
let absent = 0;
|
|
524
|
+
for (const raw of ctx.assets) {
|
|
525
|
+
const asset = bindSkillAgent(raw, ctx.agentName), dir = join(target.skillDirectory, asset.name), entries = await tree.list(dir);
|
|
526
|
+
if (entries === null || entries.length === 0) {
|
|
527
|
+
missing.push({ target, asset });
|
|
528
|
+
absent++;
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
if (entries.length !== 1 || entries[0] !== "SKILL.md")
|
|
532
|
+
throw skillConflict();
|
|
533
|
+
const bytes = await tree.read(join(dir, "SKILL.md"), 65536);
|
|
534
|
+
if (!bytes)
|
|
535
|
+
throw skillConflict();
|
|
536
|
+
if (!bytes.equals(Buffer.from(asset.content)))
|
|
537
|
+
modified.push({ target, asset, bytes });
|
|
538
|
+
}
|
|
539
|
+
if (absent === ctx.assets.length)
|
|
540
|
+
newlyInstalledPlatforms.push(target.id);
|
|
541
|
+
}
|
|
542
|
+
const command = await tree.read(value.command);
|
|
543
|
+
if (command && ((await lstat(value.command)).mode & 0o100) === 0)
|
|
544
|
+
throw unsafe();
|
|
545
|
+
const legacy = modified.length || command && !command.equals(value.shim) ? await legacyBundles(ctx, tree, value) : [];
|
|
546
|
+
for (const item of modified) {
|
|
547
|
+
const known = legacy.some(source => source.assets.some(asset => asset.name === item.asset.name && Buffer.from(bindSkillAgent(asset, ctx.agentName).content).equals(item.bytes)));
|
|
548
|
+
if (!known)
|
|
549
|
+
throw skillConflict();
|
|
550
|
+
const path = join(item.target.skillDirectory, item.asset.name, "SKILL.md");
|
|
551
|
+
if (!(await tree.read(path, 65536, 0o600))?.equals(item.bytes))
|
|
552
|
+
throw unsafe();
|
|
553
|
+
replacements.push({ path, before: item.bytes, after: Buffer.from(item.asset.content), mode: 0o600, label: `${item.target.id}:${item.asset.name}` });
|
|
554
|
+
if (!upgradedPlatforms.includes(item.target.id))
|
|
555
|
+
upgradedPlatforms.push(item.target.id);
|
|
556
|
+
}
|
|
557
|
+
if (command && !command.equals(value.shim)) {
|
|
558
|
+
if (!legacy.some(source => source.bundle.shim.equals(command)))
|
|
559
|
+
throw conflict();
|
|
560
|
+
if (!(await tree.read(value.command, 1048576, 0o700))?.equals(command))
|
|
561
|
+
throw unsafe();
|
|
562
|
+
// Always switch the shared stable command after every selected skill succeeds.
|
|
563
|
+
replacements.push({ path: value.command, before: command, after: value.shim, mode: 0o700, label: "command" });
|
|
564
|
+
}
|
|
565
|
+
return { missing, replacements, newlyInstalledPlatforms, upgradedPlatforms };
|
|
566
|
+
}
|
|
381
567
|
export async function installLocal(ctx) {
|
|
382
568
|
const tree = new OwnedTree(ctx.homeDir), changed = [];
|
|
383
569
|
let changesPossible = false;
|
|
384
|
-
const newlyInstalledPlatforms = [];
|
|
385
570
|
try {
|
|
386
571
|
validateTargets(ctx, tree);
|
|
387
572
|
if (!ctx.targets.length && !ctx.agentName)
|
|
388
573
|
throw new CliError("no_supported_platform", "未识别到支持的AI客户端。", "核对Claude Code、Codex、OpenClaw,或macOS上的Kimi Code、Kimi工作模式、WorkBuddy后再接入;不会自动安装客户端。");
|
|
389
574
|
const value = await bundle(ctx, tree);
|
|
390
575
|
await tree.directory(tree.home, false, async () => { });
|
|
391
|
-
|
|
392
|
-
for (const asset of ctx.assets)
|
|
393
|
-
if (await skillState(tree, target, bindSkillAgent(asset, ctx.agentName)) === "modified")
|
|
394
|
-
throw skillConflict();
|
|
395
|
-
const existingCommand = await tree.read(value.command);
|
|
396
|
-
if (existingCommand && !existingCommand.equals(value.shim))
|
|
397
|
-
throw conflict();
|
|
398
|
-
if (existingCommand && ((await lstat(value.command)).mode & 0o100) === 0)
|
|
399
|
-
throw unsafe();
|
|
576
|
+
await installPlan(ctx, tree, value);
|
|
400
577
|
if (await bundleState(tree, value) === "modified")
|
|
401
578
|
throw conflict();
|
|
402
579
|
const lockPath = join(tree.home, ".caotang", ".setup.lock");
|
|
@@ -411,13 +588,7 @@ export async function installLocal(ctx) {
|
|
|
411
588
|
const state = await bundleState(tree, value);
|
|
412
589
|
if (state === "modified")
|
|
413
590
|
throw conflict();
|
|
414
|
-
|
|
415
|
-
const statuses = await Promise.all(ctx.assets.map(asset => skillState(tree, target, bindSkillAgent(asset, ctx.agentName))));
|
|
416
|
-
if (statuses.includes("modified"))
|
|
417
|
-
throw skillConflict();
|
|
418
|
-
if (statuses.every(status => status === "missing"))
|
|
419
|
-
newlyInstalledPlatforms.push(target.id);
|
|
420
|
-
}
|
|
591
|
+
const originalPlan = await installPlan(ctx, tree, value);
|
|
421
592
|
if (state === "missing") {
|
|
422
593
|
const created = await tree.createDirectory(value.directory);
|
|
423
594
|
if (!created)
|
|
@@ -428,25 +599,79 @@ export async function installLocal(ctx) {
|
|
|
428
599
|
if (await bundleState(tree, value) !== "installed")
|
|
429
600
|
throw conflict();
|
|
430
601
|
}
|
|
431
|
-
for (const target of
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
602
|
+
for (const { target, asset } of originalPlan.missing) {
|
|
603
|
+
const dir = join(target.skillDirectory, asset.name), created = await tree.createDirectory(dir);
|
|
604
|
+
if (!created && (await tree.list(dir))?.length !== 0)
|
|
605
|
+
throw skillConflict();
|
|
606
|
+
changed.push(`${target.id}:${asset.name}`);
|
|
607
|
+
await tree.writeNew(join(dir, "SKILL.md"), Buffer.from(asset.content), 0o600);
|
|
608
|
+
if (await skillState(tree, target, asset) !== "installed")
|
|
609
|
+
throw skillConflict();
|
|
610
|
+
}
|
|
611
|
+
const plan = await installPlan(ctx, tree, value), attempted = [];
|
|
612
|
+
let recoveryJournal;
|
|
613
|
+
if (plan.replacements.length) {
|
|
614
|
+
const snapshot = Buffer.from(JSON.stringify({ formatVersion: 1, targetBundleSha: value.sha, files: plan.replacements.map(entry => ({ path: relative(tree.home, entry.path), mode: entry.mode, before: entry.before.toString("base64"), after: entry.after.toString("base64") })) }));
|
|
615
|
+
if (snapshot.length > 1048576)
|
|
616
|
+
throw unsafe();
|
|
617
|
+
recoveryJournal = join(tree.home, ".caotang", "cli", "upgrades", hash(snapshot) + ".json");
|
|
618
|
+
await tree.writeNew(recoveryJournal, snapshot, 0o600);
|
|
619
|
+
if (!(await tree.read(recoveryJournal, 1048576, 0o600))?.equals(snapshot))
|
|
620
|
+
throw unsafe();
|
|
621
|
+
await tree.directory(dirname(recoveryJournal), false, async (directories) => {
|
|
622
|
+
for (const directory of directories)
|
|
623
|
+
await directory.handle.sync();
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
try {
|
|
627
|
+
for (const entry of plan.replacements) {
|
|
628
|
+
attempted.push(entry);
|
|
629
|
+
changed.push(entry.label);
|
|
630
|
+
await tree.replaceManaged(entry);
|
|
631
|
+
}
|
|
632
|
+
if (await tree.writeNew(value.command, value.shim, 0o700))
|
|
633
|
+
changed.push("command");
|
|
634
|
+
if (!(await tree.read(value.command))?.equals(value.shim) || ((await lstat(value.command)).mode & 0o100) === 0 || await bundleState(tree, value) !== "installed")
|
|
635
|
+
throw conflict();
|
|
636
|
+
for (const target of ctx.targets)
|
|
637
|
+
for (const asset of ctx.assets)
|
|
638
|
+
if (await skillState(tree, target, bindSkillAgent(asset, ctx.agentName)) !== "installed")
|
|
639
|
+
throw skillConflict();
|
|
640
|
+
}
|
|
641
|
+
catch (error) {
|
|
642
|
+
if (!recoveryJournal)
|
|
643
|
+
throw error;
|
|
644
|
+
let restored = true;
|
|
645
|
+
for (const entry of attempted.reverse()) {
|
|
646
|
+
try {
|
|
647
|
+
const current = await tree.read(entry.path, 1048576, entry.mode);
|
|
648
|
+
if (!current?.equals(entry.before)) {
|
|
649
|
+
if (!entry.identity) {
|
|
650
|
+
restored = false;
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
await tree.replaceManaged(entry, true);
|
|
654
|
+
}
|
|
655
|
+
if (!(await tree.read(entry.path, 1048576, entry.mode))?.equals(entry.before)
|
|
656
|
+
|| entry.identity && !same(await lstat(entry.path), entry.identity))
|
|
657
|
+
restored = false;
|
|
658
|
+
}
|
|
659
|
+
catch {
|
|
660
|
+
restored = false;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
const recovery = { upgrade_status: restored ? "rolled_back" : "unknown", recovery_journal: recoveryJournal };
|
|
664
|
+
if (restored && error instanceof CliError && error.code === "installation_permission_denied") {
|
|
665
|
+
Object.assign(error.details, recovery);
|
|
666
|
+
throw error;
|
|
442
667
|
}
|
|
443
|
-
|
|
444
|
-
|
|
668
|
+
throw new CliError(restored ? "installation_upgrade_failed" : "installation_upgrade_unknown", restored ? "本地升级未完成,已核对并恢复本轮改动的原文件。" : "无法确认本地升级结果,部分受管文件可能尚未恢复。", "保留旧版本与恢复原件,请本人核对 recovery_journal;不要强制覆盖、删除、重新授权或重复业务写入。", 7, recovery);
|
|
669
|
+
}
|
|
670
|
+
return { changed, bundleSha: value.sha, bundleDirectory: value.directory, newlyInstalledPlatforms: originalPlan.newlyInstalledPlatforms, upgradedPlatforms: originalPlan.upgradedPlatforms };
|
|
445
671
|
}
|
|
446
672
|
finally {
|
|
447
673
|
release();
|
|
448
674
|
}
|
|
449
|
-
return { changed, bundleSha: value.sha, bundleDirectory: value.directory, newlyInstalledPlatforms };
|
|
450
675
|
}
|
|
451
676
|
catch (error) {
|
|
452
677
|
const safe = error instanceof CliError ? error
|
|
@@ -489,3 +714,48 @@ export async function inspectLocalInstallation(ctx) {
|
|
|
489
714
|
}
|
|
490
715
|
return { command: { installed, on_path, version: value.version, ...(installed ? { executable_path: value.command } : {}) }, platforms, issues };
|
|
491
716
|
}
|
|
717
|
+
// Read-only evidence for known platform paths, never an installation/overwrite
|
|
718
|
+
// permit. Only the current name's exact public templates count as its skills.
|
|
719
|
+
export async function inspectManagedOrigins(ctx) {
|
|
720
|
+
if (ctx.agentName === undefined)
|
|
721
|
+
throw new CliError("agent_name_required", "请用你自己的名字再运行 setup --agent <你的名字>。", "之后每条命令都带同一个 --agent <你的名字>;不会借用其他 AI 的连接。");
|
|
722
|
+
const tree = new OwnedTree(ctx.homeDir);
|
|
723
|
+
validateTargets(ctx, tree);
|
|
724
|
+
const value = await bundle(ctx, tree), assets = ctx.assets.map(asset => bindSkillAgent(asset, ctx.agentName));
|
|
725
|
+
let legacy = [];
|
|
726
|
+
try {
|
|
727
|
+
legacy = await legacyBundles(ctx, tree, value);
|
|
728
|
+
}
|
|
729
|
+
catch { /* Unverified old files provide no evidence; never promote them. */ }
|
|
730
|
+
const platforms = [];
|
|
731
|
+
for (const target of ctx.targets) {
|
|
732
|
+
const skills = [];
|
|
733
|
+
for (const asset of assets) {
|
|
734
|
+
let status;
|
|
735
|
+
try {
|
|
736
|
+
status = await skillState(tree, target, asset);
|
|
737
|
+
if (status === "modified") {
|
|
738
|
+
const dir = join(target.skillDirectory, asset.name), entries = await tree.list(dir);
|
|
739
|
+
if (entries?.join(",") === "SKILL.md") {
|
|
740
|
+
const bytes = await tree.read(join(dir, "SKILL.md"), 65536, 0o600);
|
|
741
|
+
if (bytes && legacy.some(source => source.assets.some(old => old.name === asset.name && Buffer.from(bindSkillAgent(old, ctx.agentName).content).equals(bytes))))
|
|
742
|
+
status = "outdated";
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
catch {
|
|
747
|
+
status = "unsafe";
|
|
748
|
+
}
|
|
749
|
+
skills.push({ name: asset.name, status });
|
|
750
|
+
}
|
|
751
|
+
platforms.push({ id: target.id, skill_directory: target.skillDirectory, ownKnownCount: skills.filter(skill => skill.status === "installed" || skill.status === "outdated").length, skills });
|
|
752
|
+
}
|
|
753
|
+
let command = { legacyManaged: false, on_path: false };
|
|
754
|
+
try {
|
|
755
|
+
const bytes = await tree.read(value.command, 1048576, 0o700), old = legacy.find(source => bytes?.equals(source.bundle.shim));
|
|
756
|
+
if (old)
|
|
757
|
+
command = { legacyManaged: true, on_path: await onPath(value.command, ctx.env), version: old.bundle.version, executable_path: value.command };
|
|
758
|
+
}
|
|
759
|
+
catch { /* An unsafe command must not be called a verified old command. */ }
|
|
760
|
+
return { platforms, command };
|
|
761
|
+
}
|
package/dist/local-commands.js
CHANGED
|
@@ -6,12 +6,13 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { authLogin } from "./auth.js";
|
|
7
7
|
import { doctor } from "./doctor.js";
|
|
8
8
|
import { CliError, asRecord } from "./errors.js";
|
|
9
|
-
import { inspectLocalInstallation, installLocal } from "./installation.js";
|
|
9
|
+
import { inspectLocalInstallation, inspectManagedOrigins, installLocal } from "./installation.js";
|
|
10
10
|
import { isPlatformId, platformSupported } from "./local-types.js";
|
|
11
|
-
import { detectPlatforms } from "./platforms.js";
|
|
11
|
+
import { detectPlatforms, knownSkillTargets } from "./platforms.js";
|
|
12
12
|
import { loadSkillAssets } from "./skill-assets.js";
|
|
13
|
+
import { LocalRecords } from "./local-records.js";
|
|
13
14
|
import { defaultConnectionNames, inspectAccountState, onboardingMessage } from "./onboarding.js";
|
|
14
|
-
import { setupFailureMessage } from "./connection-copy.js";
|
|
15
|
+
import { connectionCopy, setupFailureMessage } from "./connection-copy.js";
|
|
15
16
|
const root = () => fileURLToPath(new URL("../", import.meta.url));
|
|
16
17
|
const invalid = () => new CliError("invalid_arguments", "命令或参数无效。", "运行 --help;平台支持claude-code、codex、openclaw,以及仅macOS支持的kimi-code、kimi-work和workbuddy。");
|
|
17
18
|
export function selectedPlatforms(value, operatingSystem = process.platform) {
|
|
@@ -53,33 +54,97 @@ export async function localVersion(packageRoot = root()) {
|
|
|
53
54
|
export function manualUpdateInstructions() {
|
|
54
55
|
return { status: 200, body: { operation: "update", update_policy: "manual", next_action: "运行 npx @yijiancaotang/cli@latest setup --agent <你的名字> 更新到最新版。" } };
|
|
55
56
|
}
|
|
56
|
-
|
|
57
|
+
function connectionTimeInBeijing(value) {
|
|
58
|
+
if (!value)
|
|
59
|
+
return undefined;
|
|
60
|
+
const timestamp = Date.parse(value);
|
|
61
|
+
if (!Number.isFinite(timestamp))
|
|
62
|
+
return undefined;
|
|
63
|
+
const parts = new Intl.DateTimeFormat("en-GB", {
|
|
64
|
+
timeZone: "Asia/Shanghai", year: "numeric", month: "2-digit", day: "2-digit",
|
|
65
|
+
hour: "2-digit", minute: "2-digit", hourCycle: "h23",
|
|
66
|
+
}).formatToParts(timestamp);
|
|
67
|
+
const fields = Object.fromEntries(parts.map(part => [part.type, part.value]));
|
|
68
|
+
return `${fields.year}-${fields.month}-${fields.day} ${fields.hour}:${fields.minute}`;
|
|
69
|
+
}
|
|
70
|
+
const recordsFor = (ctx) => ctx.agent ? new LocalRecords(ctx.homeDir, ctx.agent, ctx.auth.environment ?? "production", new URL(ctx.auth.api.baseUrl).origin) : undefined;
|
|
71
|
+
async function targetsFor(ctx, ids, setup, assets, recorded) {
|
|
72
|
+
const inspect = (targets) => inspectManagedOrigins({ ...ctx, agentName: ctx.agent?.key, assets, targets });
|
|
57
73
|
if (ctx.agent) {
|
|
58
74
|
if (ids && ids.length !== 1)
|
|
59
75
|
throw new CliError("invalid_arguments", "每次接入只给一个 AI 平台安装技能。", "请带自己的 --agent 名字,一次只选一个 --platform;其它 AI 分别接入。");
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
76
|
+
if (ids === undefined && recorded.length === 0) {
|
|
77
|
+
const candidates = await knownSkillTargets({ ...ctx, skipInvalidCandidates: true }), origins = await inspect(candidates);
|
|
78
|
+
const own = candidates.filter(target => origins.platforms.some(platform => platform.id === target.id && platform.skill_directory === target.skillDirectory && platform.ownKnownCount > 0))
|
|
79
|
+
.map(target => ({ ...target, detected: true, evidence: "skills" }));
|
|
80
|
+
if (own.length > 0 || !ctx.agent.platform)
|
|
81
|
+
return { targets: own, origins };
|
|
82
|
+
}
|
|
83
|
+
ids = ids ?? (recorded.length ? recorded.map(target => target.id) : ctx.agent.platform ? [ctx.agent.platform] : []);
|
|
84
|
+
}
|
|
85
|
+
const redirected = (id) => id === "claude-code" ? ctx.env.CLAUDE_CONFIG_DIR !== undefined
|
|
86
|
+
: id === "openclaw" ? ctx.env.OPENCLAW_STATE_DIR !== undefined || ctx.env.OPENCLAW_HOME !== undefined
|
|
87
|
+
: id === "kimi-code" && ctx.env.KIMI_CODE_HOME !== undefined;
|
|
88
|
+
const preferred = recorded.filter(target => (!ids || ids.includes(target.id)) && !redirected(target.id));
|
|
89
|
+
if (preferred.some(target => !platformSupported(target.id, ctx.operatingSystem)))
|
|
90
|
+
throw new CliError("platform_unsupported", "已记录的技能平台不支持当前系统。", "请在原支持的系统上检查该平台;不会猜测新目录或搬动技能。", 2);
|
|
91
|
+
const discoveryIds = ids?.filter(id => !preferred.some(target => target.id === id));
|
|
92
|
+
// A recorded custom target must not cause inspection of an unrelated default
|
|
93
|
+
// directory just to rediscover the same platform.
|
|
94
|
+
const discovered = discoveryIds?.length === 0 ? [] : await detectPlatforms({ homeDir: ctx.homeDir, env: ctx.env, applicationRoots: ctx.applicationRoots, operatingSystem: ctx.operatingSystem, platformIds: discoveryIds });
|
|
95
|
+
const all = [...preferred.map(target => ({ ...target })), ...discovered];
|
|
96
|
+
const origins = await inspect(all);
|
|
97
|
+
for (const target of all) {
|
|
98
|
+
if (ctx.agent && !target.detected && origins.platforms.some(platform => platform.id === target.id && platform.skill_directory === target.skillDirectory && platform.ownKnownCount > 0)) {
|
|
99
|
+
target.detected = true;
|
|
100
|
+
target.evidence = "skills";
|
|
101
|
+
}
|
|
63
102
|
}
|
|
64
|
-
|
|
65
|
-
return ids ? all.filter(target => ids.includes(target.id)) : setup ? all.filter(target => target.detected) : all;
|
|
103
|
+
return { targets: ids ? all.filter(target => ids.includes(target.id)) : setup ? all.filter(target => target.detected) : all, origins };
|
|
66
104
|
}
|
|
67
105
|
export async function localDoctor(ctx, flags, authFailure) {
|
|
68
106
|
return doctor({
|
|
69
107
|
auth: ctx.auth, authFailure, agent: ctx.agent,
|
|
70
108
|
inspectLocal: async () => {
|
|
71
109
|
const assets = await loadSkillAssets(ctx.packageRoot ?? root());
|
|
72
|
-
|
|
110
|
+
const recorded = await recordsFor(ctx)?.targets() ?? [];
|
|
111
|
+
const selection = await targetsFor(ctx, selectedPlatforms(flags.platform, ctx.operatingSystem), false, assets, recorded);
|
|
112
|
+
const inspected = await inspectLocalInstallation({ ...ctx, agentName: ctx.agent?.key, assets, targets: selection.targets });
|
|
113
|
+
const platforms = inspected.platforms.map(platform => {
|
|
114
|
+
const origin = selection.origins.platforms.find(origin => origin.id === platform.id && origin.skill_directory === platform.skill_directory);
|
|
115
|
+
return origin ? { ...platform, skills: origin.skills } : platform;
|
|
116
|
+
});
|
|
117
|
+
const issues = inspected.issues.filter(issue => !["skill_missing", "skill_modified", "skill_unsafe"].includes(issue.code));
|
|
118
|
+
let command = inspected.command;
|
|
119
|
+
if (selection.origins.command.legacyManaged) {
|
|
120
|
+
command = { ...command, installed: true, ...selection.origins.command };
|
|
121
|
+
for (let index = issues.length - 1; index >= 0; index--)
|
|
122
|
+
if (issues[index].code === "command_modified")
|
|
123
|
+
issues.splice(index, 1);
|
|
124
|
+
issues.push({ code: "command_outdated", message: "已核对为旧版受管命令行,需要更新。", next_action: "沿用原来的名字运行 npx @yijiancaotang/cli@latest setup --agent <你的名字> 更新,不需要重新批准连接。" });
|
|
125
|
+
}
|
|
126
|
+
const skillPlatforms = selection.targets.filter(target => recorded.some(record => record.id === target.id)
|
|
127
|
+
|| selection.origins.platforms.some(origin => origin.id === target.id && origin.skill_directory === target.skillDirectory && origin.ownKnownCount > 0)).map(target => target.id);
|
|
128
|
+
return { ...inspected, command, platforms, issues, skill_platforms: skillPlatforms };
|
|
73
129
|
},
|
|
74
130
|
});
|
|
75
131
|
}
|
|
76
132
|
export async function localSetup(ctx, flags) {
|
|
77
|
-
const
|
|
133
|
+
const records = recordsFor(ctx), recorded = await records?.targets() ?? [];
|
|
134
|
+
const assets = await loadSkillAssets(ctx.packageRoot ?? root()), { targets } = await targetsFor(ctx, selectedPlatforms(flags.platform, ctx.operatingSystem), true, assets, recorded);
|
|
78
135
|
const local = { ...ctx, agentName: ctx.agent?.key, assets, targets };
|
|
79
136
|
const installed = await installLocal(local);
|
|
137
|
+
await records?.rememberInstallation(targets, assets, installed.bundleSha);
|
|
138
|
+
for (const target of targets) {
|
|
139
|
+
if (!target.detected) {
|
|
140
|
+
target.detected = true;
|
|
141
|
+
target.evidence = "skills";
|
|
142
|
+
}
|
|
143
|
+
}
|
|
80
144
|
const newEntries = installed.newlyInstalledPlatforms;
|
|
81
145
|
let authFailure;
|
|
82
146
|
let authorizationAttempted = false;
|
|
147
|
+
const currentSecrets = new Set(ctx.auth.tokenOverride ? [ctx.auth.tokenOverride] : []);
|
|
83
148
|
// Observe the actual start request after authLogin's credential/abort checks.
|
|
84
149
|
// Validation and doctor requests do not constitute a new authorization attempt.
|
|
85
150
|
const observedAuth = {
|
|
@@ -89,6 +154,8 @@ export async function localSetup(ctx, flags) {
|
|
|
89
154
|
request(path, options) {
|
|
90
155
|
if (path === "/api/v1/agent/device-auth/start" && options?.method === "POST")
|
|
91
156
|
authorizationAttempted = true;
|
|
157
|
+
if (path === "/api/v1/agent/mcp-token/validate" && typeof options?.token === "string")
|
|
158
|
+
currentSecrets.add(options.token);
|
|
92
159
|
return ctx.auth.api.request(path, options);
|
|
93
160
|
},
|
|
94
161
|
upload: (url, headers, bytes) => ctx.auth.api.upload(url, headers, bytes),
|
|
@@ -97,10 +164,12 @@ export async function localSetup(ctx, flags) {
|
|
|
97
164
|
};
|
|
98
165
|
let newAuthorizationOwner;
|
|
99
166
|
let loginIdentity;
|
|
167
|
+
let loginResult = null;
|
|
100
168
|
const names = defaultConnectionNames(targets.map(target => target.id), ctx.computerName, ctx.agent?.name);
|
|
101
169
|
try {
|
|
102
170
|
const login = await authLogin(observedAuth, { device_name: flags["device-name"] ?? names.deviceName, client_name: flags["client-name"] ?? names.clientName, noOpen: flags["no-open"] === true });
|
|
103
171
|
const who = asRecord(login.body);
|
|
172
|
+
loginResult = who;
|
|
104
173
|
if (typeof who?.user_id === "string" && typeof who.token_id === "string" && (who.kind === "manual" || who.kind === "device") && (who.scope === "read_only" || who.scope === "read_write")) {
|
|
105
174
|
loginIdentity = { userId: who.user_id, tokenId: who.token_id, kind: who.kind, scope: who.scope };
|
|
106
175
|
}
|
|
@@ -111,7 +180,7 @@ export async function localSetup(ctx, flags) {
|
|
|
111
180
|
authFailure = error instanceof CliError ? error : new CliError("authorization_delivery_unknown", "无法确认授权操作的结果。", "先在网页核对并吊销未保存的设备,不要自动重新签发。", 6);
|
|
112
181
|
}
|
|
113
182
|
let checkedIdentity;
|
|
114
|
-
const report = await doctor({ auth: ctx.auth, authFailure, agent: ctx.agent, inspectLocal: () => inspectLocalInstallation(local), onIdentity: who => { checkedIdentity = who; } });
|
|
183
|
+
const report = await doctor({ auth: ctx.auth, authFailure, agent: ctx.agent, inspectLocal: async () => ({ ...await inspectLocalInstallation(local), skill_platforms: targets.map(target => target.id) }), onIdentity: who => { checkedIdentity = who; } });
|
|
115
184
|
const body = asRecord(report.result.body);
|
|
116
185
|
if (!body)
|
|
117
186
|
throw new CliError("diagnostic_unavailable", "本地安装已执行,但无法确认最终诊断。", "先运行doctor只读核对,不要直接重新授权。", 7);
|
|
@@ -129,8 +198,32 @@ export async function localSetup(ctx, flags) {
|
|
|
129
198
|
const accountState = newEntries.length && body.ready === true && checkedIdentity ? await inspectAccountState(ctx.auth, checkedIdentity.userId) : "unknown";
|
|
130
199
|
const greeting = body.ready === true && checkedIdentity ? onboardingMessage(newEntries, accountState, checkedIdentity.scope)
|
|
131
200
|
: { account_state: "unknown", onboarding: { action: "none", newly_connected_platforms: [] }, user_message: setupFailureMessage(identityChanged ? "authorization_identity_changed" : authFailure?.code) };
|
|
201
|
+
const reused = Boolean(sameConnection && loginResult?.reused_existing === true);
|
|
202
|
+
let connection;
|
|
203
|
+
if (sameConnection && ctx.agent && checkedIdentity) {
|
|
204
|
+
if (reused) {
|
|
205
|
+
connection = ctx.auth.tokenOverride !== undefined ? { connection_name: ctx.agent.key, connection_name_source: "environment_override" }
|
|
206
|
+
: await records?.connection(checkedIdentity.tokenId, [...currentSecrets]) ?? { connection_name: ctx.agent.key, connection_name_source: "local_agent_key" };
|
|
207
|
+
}
|
|
208
|
+
else if (body.ready === true && loginResult?.new_authorization_confirmed === true && typeof loginResult.connection_name === "string" && typeof loginResult.connected_at === "string") {
|
|
209
|
+
connection = { connection_name: loginResult.connection_name, connection_name_source: "local_authorization_record", connected_at: loginResult.connected_at, connection_time_source: "device_authorization" };
|
|
210
|
+
await records?.rememberConnection(checkedIdentity.tokenId, connection, [...currentSecrets]);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const onboardingCopy = reused && checkedIdentity && (connection?.connection_name_source !== "environment_override"
|
|
214
|
+
? greeting.onboarding.action !== "none" || checkedIdentity.scope === "read_only"
|
|
215
|
+
: greeting.onboarding.action === "none" && checkedIdentity.scope === "read_only")
|
|
216
|
+
? { onboarding_message: greeting.onboarding.action !== "none" ? greeting.user_message : connectionCopy.read_only } : {};
|
|
217
|
+
const connectionDisplayTime = connectionTimeInBeijing(connection?.connected_at);
|
|
218
|
+
const reuseCopy = reused && connection && connection.connection_name_source !== "environment_override" ? {
|
|
219
|
+
user_message: "这个名字已经接过了,沿用原来的连接,不用再批准。",
|
|
220
|
+
next_action: `已沿用名为 ${connection.connection_name} 的连接${connectionDisplayTime ? `(${connectionDisplayTime} 接入)` : ""}。如果你不是它,请用你自己的名字重新运行 setup --agent <你的名字>,不要继续用这条连接。`,
|
|
221
|
+
...(connection.connection_name_source === "local_agent_key" ? { connection_name_note: "本机仅记有规范化名字,原连接名称和接入时间未记录。" } : {}),
|
|
222
|
+
} : reused && connection?.connection_name_source === "environment_override" ? {
|
|
223
|
+
reuse_notice: "本次沿用环境中提供的授权,不是本机保存的同名连接。若要独立接入,请由本人先处理环境授权覆盖,再用自己的名字运行 setup --agent <你的名字>。",
|
|
224
|
+
} : {};
|
|
132
225
|
return { exitCode: report.exitCode, result: { status: 200, body: { ...body, operation: "setup", local_installation: { changed: installed.changed, bundle_sha: installed.bundleSha }, authorization_attempted: authorizationAttempted,
|
|
133
|
-
new_authorization_confirmed: confirmed, ...(confirmed ? { new_authorization_owner_id: newAuthorizationOwner } : {}), ...greeting,
|
|
134
|
-
...(ctx.agent && !targets.length ? { generic_usage: { message: "
|
|
226
|
+
new_authorization_confirmed: confirmed, ...(confirmed ? { new_authorization_owner_id: newAuthorizationOwner } : {}), reused_existing: reused, ...connection, ...greeting, ...onboardingCopy, ...reuseCopy,
|
|
227
|
+
...(ctx.agent && !targets.length ? { generic_usage: { message: "暂未定位到这个名字的专用技能;若曾使用自定义目录,请保留原目录参数再检查。仍可按下列命令使用自己的连接。", commands: ["doctor", "context --start", "projects", "knowledge", "search", "auth status"].map(command => `caotang --agent ${shellWord(ctx.agent.name)} ${command}`) } } : {}),
|
|
135
228
|
...(body.ready === true ? { next_conversation: "请开新对话说‘开工了’。" } : {}) } } };
|
|
136
229
|
}
|