@modesto_ma/dsh-guanxiang 0.2.0-beta.2 → 0.2.0-beta.3
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 +9 -9
- package/lib/client.js +1 -1
- package/lib/client.js.map +1 -1
- package/lib/index.js +109 -138
- package/lib/index.js.map +1 -1
- package/package.json +2 -1
- package/src/client/page.tsx +1 -1
- package/src/config.ts +2 -2
- package/src/index.ts +2 -2
- package/src/repos/registry.ts +2 -4
- package/src/service.ts +2 -0
- package/src/skill-update/git.ts +3 -76
- package/src/skill-update/repos.ts +51 -0
- package/src/skill-update/tool.ts +66 -31
package/lib/index.js
CHANGED
|
@@ -610,8 +610,8 @@ const pluginConfigSchema = z.object({
|
|
|
610
610
|
*/
|
|
611
611
|
analyzeTokenRef: z.string().trim().regex(TOKEN_ENV_PATTERN, "令牌名称只能包含字母、数字和下划线,且不能以数字开头").default("GUANXIANG_ANALYZE_TOKEN"),
|
|
612
612
|
/**
|
|
613
|
-
* Author
|
|
614
|
-
*
|
|
613
|
+
* Author of SKILL improvement commits; omitted uses the user of the token
|
|
614
|
+
* dsh-repo holds for the repository's host, who is always the committer.
|
|
615
615
|
*/
|
|
616
616
|
skillUpdateAuthor: authorSchema.optional(),
|
|
617
617
|
/** Registered `ctx.subagents` provider the children start on. */
|
|
@@ -774,9 +774,9 @@ async function abortable(promise, signal) {
|
|
|
774
774
|
//#endregion
|
|
775
775
|
//#region src/skill-update/git.ts
|
|
776
776
|
/**
|
|
777
|
-
* 在拉取仓库中用 git
|
|
778
|
-
*
|
|
779
|
-
*
|
|
777
|
+
* 在拉取仓库中用 git 底层命令检查 SKILL 修改:用临时索引文件在分析时的提交上
|
|
778
|
+
* 替换文件、写出树对象,算出逐文件行数和 diff,不改动任何检出、快照或本地
|
|
779
|
+
* 分支。树对象的前缀用作新分支名,提交本身经 dsh-repo 完成。
|
|
780
780
|
*
|
|
781
781
|
* @module dsh-guanxiang/skill-update/git
|
|
782
782
|
*/
|
|
@@ -895,96 +895,6 @@ async function prepareChange(commands, dir, base, skillDir, files) {
|
|
|
895
895
|
});
|
|
896
896
|
}
|
|
897
897
|
}
|
|
898
|
-
/**
|
|
899
|
-
* Whether the remote already has a branch.
|
|
900
|
-
* @param commands - git runner and timeout.
|
|
901
|
-
* @param dir - clone directory.
|
|
902
|
-
* @param branch - branch name.
|
|
903
|
-
* @param env - network environment of the clone's address.
|
|
904
|
-
* @returns whether `refs/heads/<branch>` exists on `origin`.
|
|
905
|
-
*/
|
|
906
|
-
async function remoteBranchExists(commands, dir, branch, env) {
|
|
907
|
-
return (await commands.git([
|
|
908
|
-
"ls-remote",
|
|
909
|
-
"--heads",
|
|
910
|
-
"origin",
|
|
911
|
-
`refs/heads/${branch}`
|
|
912
|
-
], {
|
|
913
|
-
cwd: dir,
|
|
914
|
-
timeoutMs: commands.timeoutMs,
|
|
915
|
-
env
|
|
916
|
-
})).trim() !== "";
|
|
917
|
-
}
|
|
918
|
-
/**
|
|
919
|
-
* Commit a prepared tree on its base commit without touching any ref.
|
|
920
|
-
* @param commands - git runner and timeout.
|
|
921
|
-
* @param dir - clone directory.
|
|
922
|
-
* @param change - prepared tree and its base.
|
|
923
|
-
* @param message - full commit message.
|
|
924
|
-
* @param author - explicit identity; omitted uses the host's git configuration.
|
|
925
|
-
* @returns the new commit id.
|
|
926
|
-
*/
|
|
927
|
-
async function commitChange(commands, dir, change, message, author) {
|
|
928
|
-
const env = author === void 0 ? void 0 : {
|
|
929
|
-
GIT_AUTHOR_NAME: author.name,
|
|
930
|
-
GIT_AUTHOR_EMAIL: author.email,
|
|
931
|
-
GIT_COMMITTER_NAME: author.name,
|
|
932
|
-
GIT_COMMITTER_EMAIL: author.email
|
|
933
|
-
};
|
|
934
|
-
return (await commands.git([
|
|
935
|
-
"commit-tree",
|
|
936
|
-
change.tree,
|
|
937
|
-
"-p",
|
|
938
|
-
change.base,
|
|
939
|
-
"-F",
|
|
940
|
-
"-"
|
|
941
|
-
], {
|
|
942
|
-
cwd: dir,
|
|
943
|
-
timeoutMs: commands.timeoutMs,
|
|
944
|
-
input: message,
|
|
945
|
-
...env === void 0 ? {} : { env }
|
|
946
|
-
})).trim();
|
|
947
|
-
}
|
|
948
|
-
/** Push options cannot carry line breaks; collapse every whitespace run. */
|
|
949
|
-
function oneLine(text) {
|
|
950
|
-
return text.replace(/\s+/g, " ").trim();
|
|
951
|
-
}
|
|
952
|
-
/**
|
|
953
|
-
* Push a commit to a new remote branch and ask GitLab to open a merge request
|
|
954
|
-
* for it (`merge_request.*` push options). The push carries an empty lease on
|
|
955
|
-
* the branch, so it fails instead of updating a branch that appeared after
|
|
956
|
-
* {@link remoteBranchExists} was checked. A server that does not support push
|
|
957
|
-
* options rejects the push.
|
|
958
|
-
* @param commands - git runner and timeout.
|
|
959
|
-
* @param dir - clone directory.
|
|
960
|
-
* @param commit - commit to push.
|
|
961
|
-
* @param branch - new branch name.
|
|
962
|
-
* @param request - merge request target and text.
|
|
963
|
-
* @param env - network environment of the clone's address.
|
|
964
|
-
* @returns the merge request URL the server printed, if any.
|
|
965
|
-
*/
|
|
966
|
-
async function pushForMergeRequest(commands, dir, commit, branch, request, env) {
|
|
967
|
-
const options = [
|
|
968
|
-
"merge_request.create",
|
|
969
|
-
`merge_request.target=${request.target}`,
|
|
970
|
-
`merge_request.title=${oneLine(request.title)}`,
|
|
971
|
-
`merge_request.description=${oneLine(request.description)}`
|
|
972
|
-
].flatMap((option) => ["-o", option]);
|
|
973
|
-
const output = await commands.git([
|
|
974
|
-
"push",
|
|
975
|
-
"--porcelain",
|
|
976
|
-
`--force-with-lease=refs/heads/${branch}:`,
|
|
977
|
-
...options,
|
|
978
|
-
"origin",
|
|
979
|
-
`${commit}:refs/heads/${branch}`
|
|
980
|
-
], {
|
|
981
|
-
cwd: dir,
|
|
982
|
-
timeoutMs: commands.timeoutMs,
|
|
983
|
-
env,
|
|
984
|
-
output: "all"
|
|
985
|
-
});
|
|
986
|
-
return /https?:\/\/\S+\/-\/merge_requests\/\d+/.exec(output)?.[0];
|
|
987
|
-
}
|
|
988
898
|
//#endregion
|
|
989
899
|
//#region src/repos/manifest.ts
|
|
990
900
|
/**
|
|
@@ -1206,6 +1116,37 @@ async function readProposal(workDir, limits) {
|
|
|
1206
1116
|
};
|
|
1207
1117
|
}
|
|
1208
1118
|
//#endregion
|
|
1119
|
+
//#region src/skill-update/repos.ts
|
|
1120
|
+
/** Name dsh-repo provides the service under. */
|
|
1121
|
+
const DSH_REPOS_SERVICE = "dshRepos";
|
|
1122
|
+
/**
|
|
1123
|
+
* Classify the value `ctx.get('dshRepos')` returned. The provider is another
|
|
1124
|
+
* package that may be older or newer than this one, so its version is checked
|
|
1125
|
+
* at run time; `getBranch` marks a provider that also honors `baseSha` and
|
|
1126
|
+
* `newBranch` on commits.
|
|
1127
|
+
* @param value - the looked-up service, or undefined.
|
|
1128
|
+
* @returns the usable service, or why there is none.
|
|
1129
|
+
*/
|
|
1130
|
+
function lookupRepos(value) {
|
|
1131
|
+
if (value === void 0 || value === null) return { availability: "missing" };
|
|
1132
|
+
const service = value;
|
|
1133
|
+
if (service.version !== 1 || typeof service.getBranch !== "function" || typeof service.commitFiles !== "function" || typeof service.createChange !== "function") return { availability: "incompatible" };
|
|
1134
|
+
return {
|
|
1135
|
+
availability: "ready",
|
|
1136
|
+
service: value
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* The failure class of a `dshRepos` rejection.
|
|
1141
|
+
* @param error - the rejection.
|
|
1142
|
+
* @returns its `code` when it is a `RepoError`.
|
|
1143
|
+
*/
|
|
1144
|
+
function repoErrorCode(error) {
|
|
1145
|
+
if (!(error instanceof Error) || error.name !== "RepoError") return void 0;
|
|
1146
|
+
const code = error.code;
|
|
1147
|
+
return typeof code === "string" ? code : void 0;
|
|
1148
|
+
}
|
|
1149
|
+
//#endregion
|
|
1209
1150
|
//#region src/skill-update/tool.ts
|
|
1210
1151
|
/**
|
|
1211
1152
|
* `guanxiang_propose_skill`:把某个派发任务的 SKILL 修改提议提交为合并请求。
|
|
@@ -1213,11 +1154,13 @@ async function readProposal(workDir, limits) {
|
|
|
1213
1154
|
* - 只接受本进程中由同一会话派发过的任务运行目录;应用、仓库和分析时的提交
|
|
1214
1155
|
* 取自派发记录,不取自运行目录里可被 Agent 改写的内容。
|
|
1215
1156
|
* - 只改应用 SKILL 目录 `.dsh/skills/<skill>/` 下的文件,提交以分析时的提交为
|
|
1216
|
-
*
|
|
1217
|
-
*
|
|
1218
|
-
* -
|
|
1219
|
-
*
|
|
1220
|
-
*
|
|
1157
|
+
* 父提交,提交到新分支 `guanxiang/skill-<应用 id>-<树对象前缀>`,从不提交到
|
|
1158
|
+
* 已有分支或默认分支。
|
|
1159
|
+
* - 提交和创建合并请求经 dsh-repo 的 `dshRepos` 服务完成,支持 GitLab 与
|
|
1160
|
+
* GitHub;令牌由 dsh-repo 按主机配置。没有该服务或版本不符时工具报错。
|
|
1161
|
+
* - 提交前经 DSH 审批服务(`ctx.approval`)请求一次性授权;dsh_channels 把它
|
|
1162
|
+
* 显示为飞书决策卡片,DSH Web 显示为审批提示。只有 `allowed-once` 才提交;
|
|
1163
|
+
* 没有审批服务时工具报错,不提交。
|
|
1221
1164
|
* - 子会话中不提供本工具。
|
|
1222
1165
|
*
|
|
1223
1166
|
* @module dsh-guanxiang/skill-update/tool
|
|
@@ -1291,7 +1234,8 @@ const OUTPUT_SCHEMA$1 = {
|
|
|
1291
1234
|
}
|
|
1292
1235
|
},
|
|
1293
1236
|
diffPath: { type: "string" },
|
|
1294
|
-
mergeRequestUrl: { type: "string" }
|
|
1237
|
+
mergeRequestUrl: { type: "string" },
|
|
1238
|
+
mergeRequestError: { type: "string" }
|
|
1295
1239
|
}
|
|
1296
1240
|
};
|
|
1297
1241
|
/**
|
|
@@ -1312,12 +1256,12 @@ function renderProposeValue(value) {
|
|
|
1312
1256
|
return [{
|
|
1313
1257
|
type: "text",
|
|
1314
1258
|
text: `${{
|
|
1315
|
-
opened:
|
|
1316
|
-
pushed:
|
|
1317
|
-
exists: `${where}
|
|
1318
|
-
rejected: "
|
|
1319
|
-
cancelled: "
|
|
1320
|
-
unavailable: "
|
|
1259
|
+
opened: `已提交到 ${where} 并创建合并请求:${value.mergeRequestUrl ?? ""}`,
|
|
1260
|
+
pushed: `已提交到 ${where},但创建合并请求失败(${value.mergeRequestError ?? "未知原因"});请在代码托管平台上从该分支手动创建。`,
|
|
1261
|
+
exists: `${where} 已存在,同一改动可能已经提交过,本次没有提交。`,
|
|
1262
|
+
rejected: "授权被拒绝,没有提交:用户拒绝了这次修改,或会话的审批策略为 never(例如「完全访问」权限模式)。",
|
|
1263
|
+
cancelled: "授权请求被撤回,没有提交。",
|
|
1264
|
+
unavailable: "当前没有可以回答授权请求的界面,没有提交。"
|
|
1321
1265
|
}[value.status]}\n改动:${describeFiles(value.files)}\ndiff:${value.diffPath}`
|
|
1322
1266
|
}];
|
|
1323
1267
|
}
|
|
@@ -1406,6 +1350,7 @@ var SkillUpdater = class {
|
|
|
1406
1350
|
await replaceFile(diffPath, change.diff);
|
|
1407
1351
|
return {
|
|
1408
1352
|
reason: proposal.reason,
|
|
1353
|
+
files: proposal.files,
|
|
1409
1354
|
change,
|
|
1410
1355
|
target,
|
|
1411
1356
|
diffPath
|
|
@@ -1418,7 +1363,7 @@ var SkillUpdater = class {
|
|
|
1418
1363
|
definition() {
|
|
1419
1364
|
return {
|
|
1420
1365
|
name: PROPOSE_TOOL_NAME,
|
|
1421
|
-
description: "观象:把派发结果中某个应用的 SKILL
|
|
1366
|
+
description: "观象:把派发结果中某个应用的 SKILL 修改提议提交为合并请求。工具先请求用户授权,批准后提交到新分支并创建合并请求,不会直接改动默认分支。仅在派发结果标出了 SKILL 修改提议时使用;不要自己用 git 推送。",
|
|
1422
1367
|
parameters: PARAMETERS,
|
|
1423
1368
|
output: {
|
|
1424
1369
|
schema: OUTPUT_SCHEMA$1,
|
|
@@ -1433,7 +1378,7 @@ var SkillUpdater = class {
|
|
|
1433
1378
|
* @param rawArgs - model arguments.
|
|
1434
1379
|
* @param exec - execution context of the calling agent.
|
|
1435
1380
|
* @returns how the submission ended.
|
|
1436
|
-
* @throws when the arguments, task, proposal, or
|
|
1381
|
+
* @throws when the arguments, task, proposal, approval service, or `dshRepos` service are unusable, or the commit fails; nothing is committed then.
|
|
1437
1382
|
*/
|
|
1438
1383
|
async execute(rawArgs, exec) {
|
|
1439
1384
|
const parsed = argsSchema$1.safeParse(rawArgs);
|
|
@@ -1447,7 +1392,10 @@ var SkillUpdater = class {
|
|
|
1447
1392
|
if (task === void 0) throw new Error(`${parsed.data.workDir} 不是本次 DSH 运行中观象派发任务的运行目录;DSH 重启后需要重新分析`);
|
|
1448
1393
|
if (task.sessionId !== agent.session.header.id) throw new Error(`${parsed.data.workDir} 不是当前会话派发的任务`);
|
|
1449
1394
|
const approval = this.deps.approval();
|
|
1450
|
-
if (approval === void 0) throw new Error("当前 DSH 组合没有审批服务(@deepseek-ai/dsh-user-approval
|
|
1395
|
+
if (approval === void 0) throw new Error("当前 DSH 组合没有审批服务(@deepseek-ai/dsh-user-approval),观象不会在未经授权时提交");
|
|
1396
|
+
const lookup = lookupRepos(this.deps.repos());
|
|
1397
|
+
if (lookup.availability !== "ready") throw new Error(lookup.availability === "missing" ? `提交 SKILL 修改需要 dsh-repo 插件提供的 ${DSH_REPOS_SERVICE} 服务;请安装 dsh-repo,并在它的 hosts 中配置该应用仓库所在的主机和令牌` : `dsh-repo 的 ${DSH_REPOS_SERVICE} 服务与观象不兼容(观象需要服务版本 1 且提供 getBranch,即 dsh-repo 0.1.0-beta.2 及以上);请升级 dsh-repo`);
|
|
1398
|
+
const repos = lookup.service;
|
|
1451
1399
|
const prepared = await this.prepare(task);
|
|
1452
1400
|
if (prepared === void 0) throw new Error(`${task.workDir} 中没有 SKILL 修改提议(${PROPOSAL_DIR}/${REASON_FILE} 与 ${PROPOSAL_DIR}/${FILES_DIR}/)`);
|
|
1453
1401
|
const { change, target, diffPath, reason } = prepared;
|
|
@@ -1460,17 +1408,13 @@ var SkillUpdater = class {
|
|
|
1460
1408
|
files: [...change.files],
|
|
1461
1409
|
diffPath
|
|
1462
1410
|
};
|
|
1463
|
-
|
|
1464
|
-
git: this.deps.git,
|
|
1465
|
-
timeoutMs: this.deps.gitTimeoutMs
|
|
1466
|
-
};
|
|
1467
|
-
if (await this.deps.registry.withClone(task.url, (clone) => remoteBranchExists(commands, clone.dir, branch, clone.env))) return {
|
|
1411
|
+
if (await repos.getBranch(task.url, branch, { signal: exec.signal }) !== void 0) return {
|
|
1468
1412
|
...base,
|
|
1469
1413
|
status: "exists"
|
|
1470
1414
|
};
|
|
1471
1415
|
const preview = reason.replace(/\s+/g, " ").slice(0, REASON_PREVIEW_CHARS);
|
|
1472
|
-
const zh =
|
|
1473
|
-
const en = `GuanXiang will
|
|
1416
|
+
const zh = `观象将在 ${task.url} 提交新分支 ${branch},并创建合并请求合入 ${target}。改动:${describeFiles(change.files)}。摘要:${summary}。理由:${preview}`;
|
|
1417
|
+
const en = `GuanXiang will commit branch ${branch} to ${task.url} and open a merge request into ${target}. Files: ${change.files.map((file) => file.path).join(", ")}. Summary: ${summary}`;
|
|
1474
1418
|
const outcome = await approval.request({
|
|
1475
1419
|
agent,
|
|
1476
1420
|
toolName: PROPOSE_TOOL_NAME,
|
|
@@ -1489,23 +1433,50 @@ var SkillUpdater = class {
|
|
|
1489
1433
|
};
|
|
1490
1434
|
const message = `${summary}\n\n${reason}\n\nProposed-by: 观象 GuanXiang\nAnalyzed-commit: ${task.commit}\nSession: ${task.sessionId}\n`;
|
|
1491
1435
|
const description = `由观象在日志分析中提出(应用 ${task.app.id},分析提交 ${task.commit.slice(0, 12)},会话 ${task.sessionId})。理由:${reason.slice(0, REASON_DESCRIPTION_CHARS)}`;
|
|
1492
|
-
const
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1436
|
+
const changed = new Set(change.files.map((file) => file.path));
|
|
1437
|
+
const files = prepared.files.map((file) => ({
|
|
1438
|
+
action: "write",
|
|
1439
|
+
path: `${skillDirOf(task.app)}/${file.path}`,
|
|
1440
|
+
content: file.content
|
|
1441
|
+
})).filter((file) => changed.has(file.path));
|
|
1442
|
+
exec.signal.throwIfAborted();
|
|
1443
|
+
try {
|
|
1444
|
+
await repos.commitFiles(task.url, {
|
|
1445
|
+
branch,
|
|
1446
|
+
baseSha: task.commit,
|
|
1447
|
+
newBranch: true,
|
|
1448
|
+
message,
|
|
1449
|
+
files,
|
|
1450
|
+
...this.deps.author === void 0 ? {} : { author: this.deps.author },
|
|
1451
|
+
signal: exec.signal
|
|
1452
|
+
});
|
|
1453
|
+
} catch (error) {
|
|
1454
|
+
if (repoErrorCode(error) === "conflict") return {
|
|
1455
|
+
...base,
|
|
1456
|
+
status: "exists"
|
|
1457
|
+
};
|
|
1458
|
+
throw error;
|
|
1459
|
+
}
|
|
1460
|
+
try {
|
|
1461
|
+
const opened = await repos.createChange(task.url, {
|
|
1497
1462
|
title: summary,
|
|
1498
|
-
description
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
}
|
|
1463
|
+
body: description,
|
|
1464
|
+
sourceBranch: branch,
|
|
1465
|
+
targetBranch: target,
|
|
1466
|
+
signal: exec.signal
|
|
1467
|
+
});
|
|
1468
|
+
return {
|
|
1469
|
+
...base,
|
|
1470
|
+
status: "opened",
|
|
1471
|
+
mergeRequestUrl: opened.webUrl
|
|
1472
|
+
};
|
|
1473
|
+
} catch (error) {
|
|
1474
|
+
return {
|
|
1475
|
+
...base,
|
|
1476
|
+
status: "pushed",
|
|
1477
|
+
mergeRequestError: messageOf$2(error)
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1509
1480
|
}
|
|
1510
1481
|
};
|
|
1511
1482
|
//#endregion
|
|
@@ -2472,8 +2443,7 @@ var RepoRegistry = class {
|
|
|
2472
2443
|
const branch = entry.spec.branch ?? (await this.remoteHead(entry.dir, env)).replace(/^refs\/remotes\/origin\//, "");
|
|
2473
2444
|
return await task({
|
|
2474
2445
|
dir: entry.dir,
|
|
2475
|
-
branch
|
|
2476
|
-
env
|
|
2446
|
+
branch
|
|
2477
2447
|
});
|
|
2478
2448
|
});
|
|
2479
2449
|
}
|
|
@@ -3033,7 +3003,8 @@ var GuanxiangService = class {
|
|
|
3033
3003
|
git,
|
|
3034
3004
|
gitTimeoutMs: config.gitTimeoutMs,
|
|
3035
3005
|
...config.skillUpdateAuthor === void 0 ? {} : { author: config.skillUpdateAuthor },
|
|
3036
|
-
approval: () => ctx.get?.("approval")
|
|
3006
|
+
approval: () => ctx.get?.("approval"),
|
|
3007
|
+
repos: () => ctx.get?.(DSH_REPOS_SERVICE)
|
|
3037
3008
|
});
|
|
3038
3009
|
this.dispatcher = new Dispatcher({
|
|
3039
3010
|
registry: this.registry,
|
|
@@ -3485,8 +3456,8 @@ function mountSettingsRoutes(ctx, service) {
|
|
|
3485
3456
|
* 任务匹配已注册应用时由顶层 Agent 自行加载)和
|
|
3486
3457
|
* `guanxiang_dispatch` 工具(按应用派发子 Agent,工作目录为该应用的代码
|
|
3487
3458
|
* 仓库,首条消息用 `/<skill>` 加载仓库里的日志分析 SKILL),以及开启 SKILL
|
|
3488
|
-
* 改进时的 `guanxiang_propose_skill`
|
|
3489
|
-
*
|
|
3459
|
+
* 改进时的 `guanxiang_propose_skill` 工具(经用户授权后经 dsh-repo 把子 Agent
|
|
3460
|
+
* 的 SKILL 修改提议提交到新分支并创建合并请求)。应用仓库在设置
|
|
3490
3461
|
* 面板中只填 git 地址,由插件拉取;应用描述来自各仓库的
|
|
3491
3462
|
* `.dsh/guanxiang.yaml`。另提供日志平台分析接口 `POST /open-api/logalyze`,
|
|
3492
3463
|
* 经 dsh-channels 的 `feishuTopics` 服务在飞书群话题中开始分析。
|