@clawos-dev/clawd 0.2.488 → 0.2.490
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/dist/app-builder-plugin/mcp-server.cjs +36 -3
- package/dist/cli.cjs +147 -38
- package/dist/dispatch/mcp-server.cjs +36 -3
- package/dist/persona-defaults/persona-clawd-butler/.claude/skills/scan-persona-for-deploy/SKILL.md +14 -2
- package/dist/share-md-viewer-error.html +1 -1
- package/dist/share-md-viewer.html +1 -1
- package/dist/share-ui/assets/guest-BVnHMXcA.css +32 -0
- package/dist/share-ui/assets/{guest-YvAFFMko.js → guest-XUxeACMw.js} +153 -143
- package/dist/share-ui/guest.html +2 -2
- package/package.json +1 -1
- package/dist/share-ui/assets/guest-Dznm1HrZ.css +0 -32
|
@@ -21317,10 +21317,22 @@ var EnvSecretSchema = external_exports.object({
|
|
|
21317
21317
|
var EnvAssetSchema = external_exports.object({
|
|
21318
21318
|
name: external_exports.string().min(1)
|
|
21319
21319
|
});
|
|
21320
|
+
var ConnectorNameSchema = external_exports.string().regex(/^[a-z0-9-]+$/, "\u8FDE\u63A5\u5668\u540D\u53EA\u80FD\u662F\u5C0F\u5199\u5B57\u6BCD\u3001\u6570\u5B57\u548C\u77ED\u6A2A\u7EBF");
|
|
21321
|
+
var EnvConnectorSchema = external_exports.object({ name: ConnectorNameSchema });
|
|
21320
21322
|
var MANIFEST_BODY = {
|
|
21321
21323
|
clis: external_exports.array(EnvCliSchema),
|
|
21322
21324
|
secrets: external_exports.array(EnvSecretSchema),
|
|
21323
|
-
assets: external_exports.array(EnvAssetSchema)
|
|
21325
|
+
assets: external_exports.array(EnvAssetSchema),
|
|
21326
|
+
/**
|
|
21327
|
+
* 装了哪些连接器(design 2026-09-09-cloud-connector-credentials §7.1)。**字符串数组**,
|
|
21328
|
+
* 是四段里唯一不是 `{name}` 对象数组的一段:连接器没有任何附加属性,包一层对象只是多一层壳。
|
|
21329
|
+
*
|
|
21330
|
+
* 不复用 secrets:那段说的是「需要一份值,由部署者填」,这里没有值要填;
|
|
21331
|
+
* 不从 clis 推断:lark-cli 在底座里,每个 persona 都有它,推不出任何东西。
|
|
21332
|
+
*
|
|
21333
|
+
* **缺省空而不是必填**:本机上已有的单据不用为此重扫一遍;没声明就是没装,扫描器照样会报。
|
|
21334
|
+
*/
|
|
21335
|
+
connectors: external_exports.array(ConnectorNameSchema).default([])
|
|
21324
21336
|
};
|
|
21325
21337
|
var EnvManifestBodySchema = external_exports.object(MANIFEST_BODY);
|
|
21326
21338
|
var EnvironmentManifestSchema = external_exports.object({
|
|
@@ -21453,8 +21465,9 @@ var DeployRerunJobArgsSchema = external_exports.object({
|
|
|
21453
21465
|
}).strict();
|
|
21454
21466
|
var DeployReportFindingArgsSchema = external_exports.object({
|
|
21455
21467
|
personaId: external_exports.string().min(1),
|
|
21456
|
-
kind: external_exports.enum(["cli", "secret", "asset"]),
|
|
21468
|
+
kind: external_exports.enum(["cli", "secret", "asset", "connector"]),
|
|
21457
21469
|
// 该条目在 manifest 对应数组里的内容(形状随 kind 变,handler 按 kind 收窄校验)
|
|
21470
|
+
// connector 的 item 只有 name,落进清单时只留名字(manifest.connectors 是字符串数组)
|
|
21458
21471
|
item: external_exports.record(external_exports.string(), external_exports.unknown()),
|
|
21459
21472
|
// 这条发现的来历(一句人话 + 原文出处),形状同 EnvEvidence,target 由 handler 从 kind+name 派生
|
|
21460
21473
|
evidence: external_exports.object({
|
|
@@ -21750,6 +21763,16 @@ var DispatchRecordSchema = external_exports.object({
|
|
|
21750
21763
|
sourceSessionId: external_exports.string().min(1),
|
|
21751
21764
|
sourcePersonaId: external_exports.string().min(1).optional(),
|
|
21752
21765
|
targetPersonaId: external_exports.string().min(1),
|
|
21766
|
+
/**
|
|
21767
|
+
* 跨设备派活时 B 所在的设备(`DispatchRunArgs.targetDeviceId`),A 端台账记「我派给了谁的机器」。
|
|
21768
|
+
*
|
|
21769
|
+
* **缺省即本机**,与入参侧用 `undefined` 表示本机是同一条口径——不引第二种表示法。
|
|
21770
|
+
* 存量台账行没有这个字段,消费方(dispatch 卡片的归属带)据此只显示 persona、不显示设备。
|
|
21771
|
+
*
|
|
21772
|
+
* B 端自己的台账行**不会**有它:peer-forward 转发时故意 strip 掉(B 据此判定本地执行),
|
|
21773
|
+
* 那条行记的是 B 视角的本地 dispatch。
|
|
21774
|
+
*/
|
|
21775
|
+
targetDeviceId: external_exports.string().min(1).optional(),
|
|
21753
21776
|
/** B 的 sessionId;registerBSession 后回填 */
|
|
21754
21777
|
workerSessionId: external_exports.string().min(1).optional(),
|
|
21755
21778
|
/** 任务摘要;截断(≤500 字符)在 daemon store.add 处保证 */
|
|
@@ -21776,7 +21799,17 @@ var DispatchRecordSchema = external_exports.object({
|
|
|
21776
21799
|
deliveredAt: external_exports.string().min(1).optional()
|
|
21777
21800
|
});
|
|
21778
21801
|
var DispatchListArgsSchema = external_exports.object({
|
|
21779
|
-
sourceSessionId: external_exports.string().min(1).optional()
|
|
21802
|
+
sourceSessionId: external_exports.string().min(1).optional(),
|
|
21803
|
+
/**
|
|
21804
|
+
* 「**这条会话卷入的**所有台账行」:`sourceSessionId === it || workerSessionId === it`。
|
|
21805
|
+
*
|
|
21806
|
+
* 为什么不复用 `sourceSessionId`:一条会话既可能是派活方(在自己这儿看回报卡),也可能是
|
|
21807
|
+
* 干活方(在 worker 会话里看任务包),后者用 sourceSessionId 过滤一条也查不到。UI 的
|
|
21808
|
+
* dispatch 卡片要的正是「跟我这条会话有关的全部 dispatch」,一次调用答完,不必拉全量再本地筛。
|
|
21809
|
+
*
|
|
21810
|
+
* 与 `sourceSessionId` 同时给时两个条件都要满足(AND),但调用方没有这么用的场景。
|
|
21811
|
+
*/
|
|
21812
|
+
relatedSessionId: external_exports.string().min(1).optional()
|
|
21780
21813
|
});
|
|
21781
21814
|
var DispatchListResultSchema = external_exports.object({
|
|
21782
21815
|
records: external_exports.array(DispatchRecordSchema)
|
package/dist/cli.cjs
CHANGED
|
@@ -5951,7 +5951,7 @@ var init_contact_handshake = __esm({
|
|
|
5951
5951
|
});
|
|
5952
5952
|
|
|
5953
5953
|
// ../protocol/src/environment-schemas.ts
|
|
5954
|
-
var EnvCliSchema, EnvSecretSchema, EnvAssetSchema, MANIFEST_BODY, EnvManifestBodySchema, EnvironmentManifestSchema, EnvEvidenceSchema;
|
|
5954
|
+
var EnvCliSchema, EnvSecretSchema, EnvAssetSchema, ConnectorNameSchema, EnvConnectorSchema, MANIFEST_BODY, EnvManifestBodySchema, EnvironmentManifestSchema, EnvEvidenceSchema;
|
|
5955
5955
|
var init_environment_schemas = __esm({
|
|
5956
5956
|
"../protocol/src/environment-schemas.ts"() {
|
|
5957
5957
|
"use strict";
|
|
@@ -5993,10 +5993,22 @@ var init_environment_schemas = __esm({
|
|
|
5993
5993
|
EnvAssetSchema = external_exports.object({
|
|
5994
5994
|
name: external_exports.string().min(1)
|
|
5995
5995
|
});
|
|
5996
|
+
ConnectorNameSchema = external_exports.string().regex(/^[a-z0-9-]+$/, "\u8FDE\u63A5\u5668\u540D\u53EA\u80FD\u662F\u5C0F\u5199\u5B57\u6BCD\u3001\u6570\u5B57\u548C\u77ED\u6A2A\u7EBF");
|
|
5997
|
+
EnvConnectorSchema = external_exports.object({ name: ConnectorNameSchema });
|
|
5996
5998
|
MANIFEST_BODY = {
|
|
5997
5999
|
clis: external_exports.array(EnvCliSchema),
|
|
5998
6000
|
secrets: external_exports.array(EnvSecretSchema),
|
|
5999
|
-
assets: external_exports.array(EnvAssetSchema)
|
|
6001
|
+
assets: external_exports.array(EnvAssetSchema),
|
|
6002
|
+
/**
|
|
6003
|
+
* 装了哪些连接器(design 2026-09-09-cloud-connector-credentials §7.1)。**字符串数组**,
|
|
6004
|
+
* 是四段里唯一不是 `{name}` 对象数组的一段:连接器没有任何附加属性,包一层对象只是多一层壳。
|
|
6005
|
+
*
|
|
6006
|
+
* 不复用 secrets:那段说的是「需要一份值,由部署者填」,这里没有值要填;
|
|
6007
|
+
* 不从 clis 推断:lark-cli 在底座里,每个 persona 都有它,推不出任何东西。
|
|
6008
|
+
*
|
|
6009
|
+
* **缺省空而不是必填**:本机上已有的单据不用为此重扫一遍;没声明就是没装,扫描器照样会报。
|
|
6010
|
+
*/
|
|
6011
|
+
connectors: external_exports.array(ConnectorNameSchema).default([])
|
|
6000
6012
|
};
|
|
6001
6013
|
EnvManifestBodySchema = external_exports.object(MANIFEST_BODY);
|
|
6002
6014
|
EnvironmentManifestSchema = external_exports.object({
|
|
@@ -6148,8 +6160,9 @@ var init_deploy_schemas = __esm({
|
|
|
6148
6160
|
}).strict();
|
|
6149
6161
|
DeployReportFindingArgsSchema = external_exports.object({
|
|
6150
6162
|
personaId: external_exports.string().min(1),
|
|
6151
|
-
kind: external_exports.enum(["cli", "secret", "asset"]),
|
|
6163
|
+
kind: external_exports.enum(["cli", "secret", "asset", "connector"]),
|
|
6152
6164
|
// 该条目在 manifest 对应数组里的内容(形状随 kind 变,handler 按 kind 收窄校验)
|
|
6165
|
+
// connector 的 item 只有 name,落进清单时只留名字(manifest.connectors 是字符串数组)
|
|
6153
6166
|
item: external_exports.record(external_exports.string(), external_exports.unknown()),
|
|
6154
6167
|
// 这条发现的来历(一句人话 + 原文出处),形状同 EnvEvidence,target 由 handler 从 kind+name 派生
|
|
6155
6168
|
evidence: external_exports.object({
|
|
@@ -6494,6 +6507,16 @@ var init_dispatch = __esm({
|
|
|
6494
6507
|
sourceSessionId: external_exports.string().min(1),
|
|
6495
6508
|
sourcePersonaId: external_exports.string().min(1).optional(),
|
|
6496
6509
|
targetPersonaId: external_exports.string().min(1),
|
|
6510
|
+
/**
|
|
6511
|
+
* 跨设备派活时 B 所在的设备(`DispatchRunArgs.targetDeviceId`),A 端台账记「我派给了谁的机器」。
|
|
6512
|
+
*
|
|
6513
|
+
* **缺省即本机**,与入参侧用 `undefined` 表示本机是同一条口径——不引第二种表示法。
|
|
6514
|
+
* 存量台账行没有这个字段,消费方(dispatch 卡片的归属带)据此只显示 persona、不显示设备。
|
|
6515
|
+
*
|
|
6516
|
+
* B 端自己的台账行**不会**有它:peer-forward 转发时故意 strip 掉(B 据此判定本地执行),
|
|
6517
|
+
* 那条行记的是 B 视角的本地 dispatch。
|
|
6518
|
+
*/
|
|
6519
|
+
targetDeviceId: external_exports.string().min(1).optional(),
|
|
6497
6520
|
/** B 的 sessionId;registerBSession 后回填 */
|
|
6498
6521
|
workerSessionId: external_exports.string().min(1).optional(),
|
|
6499
6522
|
/** 任务摘要;截断(≤500 字符)在 daemon store.add 处保证 */
|
|
@@ -6520,7 +6543,17 @@ var init_dispatch = __esm({
|
|
|
6520
6543
|
deliveredAt: external_exports.string().min(1).optional()
|
|
6521
6544
|
});
|
|
6522
6545
|
DispatchListArgsSchema = external_exports.object({
|
|
6523
|
-
sourceSessionId: external_exports.string().min(1).optional()
|
|
6546
|
+
sourceSessionId: external_exports.string().min(1).optional(),
|
|
6547
|
+
/**
|
|
6548
|
+
* 「**这条会话卷入的**所有台账行」:`sourceSessionId === it || workerSessionId === it`。
|
|
6549
|
+
*
|
|
6550
|
+
* 为什么不复用 `sourceSessionId`:一条会话既可能是派活方(在自己这儿看回报卡),也可能是
|
|
6551
|
+
* 干活方(在 worker 会话里看任务包),后者用 sourceSessionId 过滤一条也查不到。UI 的
|
|
6552
|
+
* dispatch 卡片要的正是「跟我这条会话有关的全部 dispatch」,一次调用答完,不必拉全量再本地筛。
|
|
6553
|
+
*
|
|
6554
|
+
* 与 `sourceSessionId` 同时给时两个条件都要满足(AND),但调用方没有这么用的场景。
|
|
6555
|
+
*/
|
|
6556
|
+
relatedSessionId: external_exports.string().min(1).optional()
|
|
6524
6557
|
});
|
|
6525
6558
|
DispatchListResultSchema = external_exports.object({
|
|
6526
6559
|
records: external_exports.array(DispatchRecordSchema)
|
|
@@ -48974,11 +49007,14 @@ var SessionManager = class {
|
|
|
48974
49007
|
* 该群的专属 guest session,turn-idle 后从 reducer buffer 提取回复文本。
|
|
48975
49008
|
*
|
|
48976
49009
|
* 跟 createShiftSession 的差异:
|
|
48977
|
-
* - **find-or-create**:按 capId `lark-<chatId>` 找现存 session
|
|
48978
|
-
*
|
|
49010
|
+
* - **find-or-create**:按 capId `lark-<chatId>` 找现存 session,没有才建;daemon
|
|
49011
|
+
* `create` 的无 dedupe 语义不变——dedupe 是本方法私有实现。
|
|
49012
|
+
* 粒度由云端给的 chatId 决定,daemon 只当它是不透明 key:群消息是虚拟三段式
|
|
49013
|
+
* `<真实 chat_id>:<thread_id>:<open_id>`(spec 2026-09-10,一话题一人一条),
|
|
49014
|
+
* p2p 是裸 chat_id,文档评论是 `doc:<fileToken>`
|
|
48979
49015
|
* - guest scope + 沙箱:群成员 query 不带 owner 权限;10min guest idle-kill + resume
|
|
48980
|
-
* - turn-idle 后**不 kill**(session
|
|
48981
|
-
* text 落盘(schemas.ts turn_end
|
|
49016
|
+
* - turn-idle 后**不 kill**(session 常驻,记忆即价值);TUI 下延迟 settleDelayMs 等
|
|
49017
|
+
* 尾随 text 落盘(schemas.ts turn_end 注释 ②)再读 buffer,SDK / codex 同步读
|
|
48982
49018
|
* - 回复提取:extractLarkReplyText(最后一条 user_text 之后的 text 事件拼接)
|
|
48983
49019
|
*/
|
|
48984
49020
|
runLarkChatTurn(args) {
|
|
@@ -49045,21 +49081,23 @@ var SessionManager = class {
|
|
|
49045
49081
|
if (!r) return;
|
|
49046
49082
|
r.input({ kind: "command", command: { kind: "stop" } });
|
|
49047
49083
|
};
|
|
49048
|
-
const settleDelayMs = args.settleDelayMs ?? 500;
|
|
49084
|
+
const settleDelayMs = args.settleDelayMs ?? (this.deps.mode === "tui" ? 500 : 0);
|
|
49085
|
+
const extractAndSettle = () => {
|
|
49086
|
+
try {
|
|
49087
|
+
const events = this.readHistoryEventsForScope(sessionId, scope);
|
|
49088
|
+
const replyText = extractLarkReplyText(events);
|
|
49089
|
+
settle(replyText ? { ok: true, replyText } : { error: "empty reply" });
|
|
49090
|
+
} catch (err) {
|
|
49091
|
+
settle({ error: err.message });
|
|
49092
|
+
}
|
|
49093
|
+
};
|
|
49049
49094
|
unsubscribe = this.subscribe(
|
|
49050
49095
|
sessionId,
|
|
49051
49096
|
scope,
|
|
49052
49097
|
makeEndFrameHandler(sessionId, (info) => {
|
|
49053
49098
|
if (info.kind === "idle") {
|
|
49054
|
-
|
|
49055
|
-
|
|
49056
|
-
const events = this.readHistoryEventsForScope(sessionId, scope);
|
|
49057
|
-
const replyText = extractLarkReplyText(events);
|
|
49058
|
-
settle(replyText ? { ok: true, replyText } : { error: "empty reply" });
|
|
49059
|
-
} catch (err) {
|
|
49060
|
-
settle({ error: err.message });
|
|
49061
|
-
}
|
|
49062
|
-
}, settleDelayMs);
|
|
49099
|
+
if (settleDelayMs === 0) extractAndSettle();
|
|
49100
|
+
else setTimeout(extractAndSettle, settleDelayMs);
|
|
49063
49101
|
} else if (info.kind === "stopped") {
|
|
49064
49102
|
settle({ error: "session stopped before reply" });
|
|
49065
49103
|
} else {
|
|
@@ -53662,6 +53700,7 @@ var PersonaDispatchManager = class {
|
|
|
53662
53700
|
sourceSessionId: args.sourceSessionId,
|
|
53663
53701
|
...args.sourcePersonaId ? { sourcePersonaId: args.sourcePersonaId } : {},
|
|
53664
53702
|
targetPersonaId: args.targetPersona,
|
|
53703
|
+
...args.targetDeviceId ? { targetDeviceId: args.targetDeviceId } : {},
|
|
53665
53704
|
taskText: args.taskText,
|
|
53666
53705
|
...args.taskBrief ? { taskBrief: args.taskBrief } : {},
|
|
53667
53706
|
...args.meta ? { meta: args.meta } : {}
|
|
@@ -53779,6 +53818,7 @@ function createDispatchStore(deps) {
|
|
|
53779
53818
|
sourceSessionId: input.sourceSessionId,
|
|
53780
53819
|
...input.sourcePersonaId ? { sourcePersonaId: input.sourcePersonaId } : {},
|
|
53781
53820
|
targetPersonaId: input.targetPersonaId,
|
|
53821
|
+
...input.targetDeviceId ? { targetDeviceId: input.targetDeviceId } : {},
|
|
53782
53822
|
taskText: input.taskText.slice(0, TASK_TEXT_MAX),
|
|
53783
53823
|
...input.taskBrief ? { taskBrief: input.taskBrief } : {},
|
|
53784
53824
|
// 新开 dispatch = 第 1 轮;续派在 handler 里 +1
|
|
@@ -53800,7 +53840,9 @@ function createDispatchStore(deps) {
|
|
|
53800
53840
|
return merged;
|
|
53801
53841
|
}
|
|
53802
53842
|
function list(filter) {
|
|
53803
|
-
return records.filter((r) => !filter?.sourceSessionId || r.sourceSessionId === filter.sourceSessionId).
|
|
53843
|
+
return records.filter((r) => !filter?.sourceSessionId || r.sourceSessionId === filter.sourceSessionId).filter(
|
|
53844
|
+
(r) => !filter?.relatedSessionId || r.sourceSessionId === filter.relatedSessionId || r.workerSessionId === filter.relatedSessionId
|
|
53845
|
+
).slice().sort((a, b2) => a.createdAt < b2.createdAt ? 1 : a.createdAt > b2.createdAt ? -1 : 0);
|
|
53804
53846
|
}
|
|
53805
53847
|
function markInterruptedAsFailed(nowIso3) {
|
|
53806
53848
|
let touched = false;
|
|
@@ -54988,11 +55030,16 @@ async function writeScan(dir, scan) {
|
|
|
54988
55030
|
await writeJson(import_node_path31.default.join(dir, SCAN_FILE), DeployScanSchema.parse(scan));
|
|
54989
55031
|
}
|
|
54990
55032
|
function emptyDeployFile() {
|
|
54991
|
-
return { version: 1, manifest: { version: 1, clis: [], secrets: [], assets: [] } };
|
|
55033
|
+
return { version: 1, manifest: { version: 1, clis: [], secrets: [], assets: [], connectors: [] } };
|
|
54992
55034
|
}
|
|
55035
|
+
var FINDING_SCHEMAS = {
|
|
55036
|
+
cli: EnvCliSchema,
|
|
55037
|
+
secret: EnvSecretSchema,
|
|
55038
|
+
asset: EnvAssetSchema,
|
|
55039
|
+
connector: EnvConnectorSchema
|
|
55040
|
+
};
|
|
54993
55041
|
function parseFinding(kind, item) {
|
|
54994
|
-
const
|
|
54995
|
-
const parsed = schema.safeParse(item);
|
|
55042
|
+
const parsed = FINDING_SCHEMAS[kind].safeParse(item);
|
|
54996
55043
|
if (!parsed.success) {
|
|
54997
55044
|
const first = parsed.error.issues[0];
|
|
54998
55045
|
throw new Error(`${kind} \u6761\u76EE\u4E0D\u5408\u6CD5\uFF1A${first ? `${first.path.join(".")} ${first.message}` : ""}`);
|
|
@@ -55000,6 +55047,11 @@ function parseFinding(kind, item) {
|
|
|
55000
55047
|
return parsed.data;
|
|
55001
55048
|
}
|
|
55002
55049
|
function upsertFinding(file, kind, item) {
|
|
55050
|
+
if (kind === "connector") {
|
|
55051
|
+
const have = file.manifest.connectors;
|
|
55052
|
+
const connectors = have.includes(item.name) ? have : [...have, item.name];
|
|
55053
|
+
return { ...file, manifest: { ...file.manifest, connectors } };
|
|
55054
|
+
}
|
|
55003
55055
|
const key = kind === "cli" ? "clis" : kind === "secret" ? "secrets" : "assets";
|
|
55004
55056
|
const list = [...file.manifest[key]];
|
|
55005
55057
|
const idx = list.findIndex((x) => x.name === item.name);
|
|
@@ -55086,6 +55138,10 @@ var L = [
|
|
|
55086
55138
|
// smoke 与 deploy 都要:smoke 得用正式那份模型探活,否则验的不是将要上线的东西。
|
|
55087
55139
|
" model: ${{ steps.plan.outputs.model }}",
|
|
55088
55140
|
" effort: ${{ steps.plan.outputs.effort }}",
|
|
55141
|
+
// 装了哪些连接器(JSON 数组)。gateway 按它决定要不要为任务签工牌、把授权链接拼进 prompt
|
|
55142
|
+
//(design 2026-09-09-cloud-connector-credentials)。名字形状在 deploy-file.mjs 卡死成 [a-z0-9-],
|
|
55143
|
+
// 所以下面单引号包住直接拼进命令行是安全的。
|
|
55144
|
+
" connectors: ${{ steps.plan.outputs.connectors }}",
|
|
55089
55145
|
" steps:",
|
|
55090
55146
|
" - name: Checkout persona",
|
|
55091
55147
|
" uses: actions/checkout@v4",
|
|
@@ -55208,6 +55264,7 @@ var L = [
|
|
|
55208
55264
|
" --persona-id '${{ needs.build.outputs.persona_id }}' \\",
|
|
55209
55265
|
" --model '${{ needs.build.outputs.model }}' \\",
|
|
55210
55266
|
" --effort '${{ needs.build.outputs.effort }}' \\",
|
|
55267
|
+
" --connectors '${{ needs.build.outputs.connectors }}' \\",
|
|
55211
55268
|
" --run-number '${{ github.run_number }}'",
|
|
55212
55269
|
"",
|
|
55213
55270
|
" deploy:",
|
|
@@ -55237,7 +55294,8 @@ var L = [
|
|
|
55237
55294
|
" --image '${{ needs.build.outputs.image }}' \\",
|
|
55238
55295
|
" --persona-id '${{ needs.build.outputs.persona_id }}' \\",
|
|
55239
55296
|
" --model '${{ needs.build.outputs.model }}' \\",
|
|
55240
|
-
" --effort '${{ needs.build.outputs.effort }}'",
|
|
55297
|
+
" --effort '${{ needs.build.outputs.effort }}' \\",
|
|
55298
|
+
" --connectors '${{ needs.build.outputs.connectors }}'",
|
|
55241
55299
|
""
|
|
55242
55300
|
];
|
|
55243
55301
|
var WORKFLOW_TEMPLATE = L.join("\n");
|
|
@@ -56413,6 +56471,18 @@ init_protocol();
|
|
|
56413
56471
|
var ERROR_REPLY_TEXT = "\u5904\u7406\u51FA\u9519\u4E86\uFF0C\u8BF7\u91CD\u8BD5";
|
|
56414
56472
|
var LOCAL_TURN_RETRIES = 2;
|
|
56415
56473
|
var STOPPED_ERROR = "session stopped before reply";
|
|
56474
|
+
var isChatOf = (key, realChatId) => key === realChatId || key.startsWith(`${realChatId}:`);
|
|
56475
|
+
var THREAD_TITLE_MAX = 20;
|
|
56476
|
+
var threadTitleOf = (env) => {
|
|
56477
|
+
const root = env.threadContext?.[0]?.text?.trim();
|
|
56478
|
+
if (!root) return void 0;
|
|
56479
|
+
return root.length > THREAD_TITLE_MAX ? `${root.slice(0, THREAD_TITLE_MAX)}\u2026` : root;
|
|
56480
|
+
};
|
|
56481
|
+
var sessionNameOf = (env) => {
|
|
56482
|
+
if (env.type === "doc_comment" || (env.chatType ?? "group") !== "group") return env.chatName;
|
|
56483
|
+
const speaker = env.senderName || env.senderOpenId;
|
|
56484
|
+
return [env.chatName, threadTitleOf(env), speaker].filter(Boolean).join("\xB7") || void 0;
|
|
56485
|
+
};
|
|
56416
56486
|
var DEBOUNCE_MS = 1e3;
|
|
56417
56487
|
var MAX_IMAGE_BYTES = 8 * 1024 * 1024;
|
|
56418
56488
|
var EXT_BY_MIME = {
|
|
@@ -56517,9 +56587,23 @@ function createLarkChatRouter(deps) {
|
|
|
56517
56587
|
if (lines.length === 0) lines.push("[\u6709\u4EBA\u5728\u98DE\u4E66\u6587\u6863\u91CC @ \u4E86\u4F60\uFF0C\u4F46\u8BC4\u8BBA\u5185\u5BB9\u62C9\u53D6\u5931\u8D25]");
|
|
56518
56588
|
return lines.join("\n");
|
|
56519
56589
|
};
|
|
56590
|
+
const buildThreadContextSection = async (env, batchMessageIds) => {
|
|
56591
|
+
const rows = (env.threadContext ?? []).filter((r) => !batchMessageIds.has(r.messageId));
|
|
56592
|
+
if (rows.length === 0) return void 0;
|
|
56593
|
+
const head = env.threadContextTruncated ? `[\u8BDD\u9898\u4E0A\u4E0B\u6587 \xB7 \u5171 ${rows.length} \u6761\uFF0C\u66F4\u65E9\u7684\u5DF2\u7565]` : `[\u8BDD\u9898\u4E0A\u4E0B\u6587 \xB7 \u5171 ${rows.length} \u6761]`;
|
|
56594
|
+
const lines = [head];
|
|
56595
|
+
for (const row of rows) {
|
|
56596
|
+
lines.push(`${row.senderName}\uFF1A${row.text?.trim() ?? ""}`);
|
|
56597
|
+
lines.push(...await imageLinesFor(env, row.messageId, row.imageKeys ?? []));
|
|
56598
|
+
lines.push(...await fileLinesFor(env, row.messageId, row.files ?? []));
|
|
56599
|
+
}
|
|
56600
|
+
lines.push("---");
|
|
56601
|
+
return lines.join("\n");
|
|
56602
|
+
};
|
|
56520
56603
|
const buildSection = async (env) => {
|
|
56521
56604
|
if (env.type === "doc_comment") return buildDocCommentSection(env);
|
|
56522
56605
|
const parentLines = env.parentMessageId ? [
|
|
56606
|
+
...env.parentText?.trim() ? [`[\u88AB\u5F15\u7528\u7684\u6D88\u606F] ${env.parentText.trim()}`] : [],
|
|
56523
56607
|
...await imageLinesFor(env, env.parentMessageId, env.parentImageKeys ?? []),
|
|
56524
56608
|
...await fileLinesFor(env, env.parentMessageId, env.parentFiles ?? [])
|
|
56525
56609
|
] : [];
|
|
@@ -56560,19 +56644,21 @@ ${body}
|
|
|
56560
56644
|
const section = [quoteAnnotation(env, i, idxByMessageId), sectionBody].filter(Boolean).join("\n");
|
|
56561
56645
|
if (section) sections.push(section);
|
|
56562
56646
|
}
|
|
56563
|
-
const
|
|
56647
|
+
const threadSection = await buildThreadContextSection(envs[0], new Set(idxByMessageId.keys()));
|
|
56648
|
+
const body = [threadSection, ...sections].filter(Boolean).join("\n") || "[\u975E\u6587\u672C\u6D88\u606F]";
|
|
56564
56649
|
const runTurn = (text) => {
|
|
56565
56650
|
const turn = deps.manager.runLarkChatTurn({
|
|
56566
56651
|
personaId: last.personaId,
|
|
56567
56652
|
chatId: last.chatId,
|
|
56568
|
-
chatName: last
|
|
56653
|
+
chatName: sessionNameOf(last),
|
|
56569
56654
|
chatType: last.chatType ?? "group",
|
|
56570
56655
|
senderOpenId: last.senderOpenId ?? "",
|
|
56571
56656
|
senderName: last.senderName ?? last.senderOpenId ?? "\u672A\u77E5\u6210\u5458",
|
|
56572
56657
|
text
|
|
56573
56658
|
});
|
|
56574
|
-
|
|
56575
|
-
|
|
56659
|
+
const activityChatId = last.realChatId ?? last.chatId;
|
|
56660
|
+
activity.set(activityChatId, {
|
|
56661
|
+
chatId: activityChatId,
|
|
56576
56662
|
chatName: last.chatName,
|
|
56577
56663
|
lastActiveAt: Date.now(),
|
|
56578
56664
|
personaId: last.personaId
|
|
@@ -56607,7 +56693,8 @@ ${formatLarkSpeakerReminder(speaker)}`
|
|
|
56607
56693
|
try {
|
|
56608
56694
|
await deps.cloud.reply({
|
|
56609
56695
|
appId: last.appId,
|
|
56610
|
-
chatId
|
|
56696
|
+
// 回复走真身:chatId 自 spec 2026-09-10 起是虚拟三段式,只作会话 key 用
|
|
56697
|
+
chatId: last.realChatId ?? last.chatId,
|
|
56611
56698
|
// envs 已过滤保证锚点非空
|
|
56612
56699
|
replyToMessageId: replyAnchorOf(last),
|
|
56613
56700
|
text: replyText,
|
|
@@ -56650,11 +56737,13 @@ ${formatLarkSpeakerReminder(speaker)}`
|
|
|
56650
56737
|
if (env.type === "bot_removed") {
|
|
56651
56738
|
deps.store.addRemovedChat(env.personaId, env.chatId);
|
|
56652
56739
|
activity.delete(env.chatId);
|
|
56653
|
-
queues.
|
|
56654
|
-
|
|
56655
|
-
|
|
56740
|
+
for (const key2 of [...queues.keys()]) {
|
|
56741
|
+
if (isChatOf(key2, env.chatId)) queues.delete(key2);
|
|
56742
|
+
}
|
|
56743
|
+
for (const [key2, p2] of [...pending.entries()]) {
|
|
56744
|
+
if (!isChatOf(key2, env.chatId)) continue;
|
|
56656
56745
|
clearTimeout(p2.timer);
|
|
56657
|
-
pending.delete(
|
|
56746
|
+
pending.delete(key2);
|
|
56658
56747
|
}
|
|
56659
56748
|
deps.logger?.info(`larkBot removed from chat ${env.chatId} (persona ${env.personaId})`);
|
|
56660
56749
|
return;
|
|
@@ -56812,9 +56901,10 @@ function canAccessPersona(grants, personaId2, action) {
|
|
|
56812
56901
|
|
|
56813
56902
|
// src/handlers/persona-dispatch.ts
|
|
56814
56903
|
init_claude();
|
|
56815
|
-
function buildFollowUpTaskPack(dispatchId, prompt, round) {
|
|
56904
|
+
function buildFollowUpTaskPack(dispatchId, prompt, round, from) {
|
|
56816
56905
|
return formatDispatchTaskEnvelope({
|
|
56817
56906
|
dispatchId,
|
|
56907
|
+
...from ? { from } : {},
|
|
56818
56908
|
round,
|
|
56819
56909
|
detail: [
|
|
56820
56910
|
`[Follow-up instruction for dispatch ${dispatchId}]`,
|
|
@@ -56840,6 +56930,7 @@ function buildPersonaDispatchHandlers(deps) {
|
|
|
56840
56930
|
});
|
|
56841
56931
|
}
|
|
56842
56932
|
const args = DispatchRunArgsSchema.parse(rest);
|
|
56933
|
+
const sourcePersonaId = deps.getSessionPersonaId?.(sourceSessionId);
|
|
56843
56934
|
if (args.tool !== void 0 && ctx?.principal.kind === "guest") {
|
|
56844
56935
|
throw new ClawdError(
|
|
56845
56936
|
ERROR_CODES.UNAUTHORIZED,
|
|
@@ -56895,7 +56986,14 @@ function buildPersonaDispatchHandlers(deps) {
|
|
|
56895
56986
|
});
|
|
56896
56987
|
const ok = deps.sendFollowUp(
|
|
56897
56988
|
rec4.workerSessionId,
|
|
56898
|
-
buildFollowUpTaskPack(
|
|
56989
|
+
buildFollowUpTaskPack(
|
|
56990
|
+
args.dispatchId,
|
|
56991
|
+
args.prompt,
|
|
56992
|
+
nextRound,
|
|
56993
|
+
// 台账行里有就用台账的(本机派活一律有),没有再现查一次:老台账行是在
|
|
56994
|
+
// sourcePersonaId 有写入点之前落的,那些行只能靠现查补上。
|
|
56995
|
+
rec4.sourcePersonaId ?? deps.getSessionPersonaId?.(sourceSessionId)
|
|
56996
|
+
),
|
|
56899
56997
|
// 来源分型(L2):续派并进 dispatch-task;realtime 侧靠这个 meta 打标,
|
|
56900
56998
|
// history 侧靠 classifyUserMessage 从信封解出同值。
|
|
56901
56999
|
"dispatch-task"
|
|
@@ -56961,6 +57059,10 @@ function buildPersonaDispatchHandlers(deps) {
|
|
|
56961
57059
|
const { dispatchId: dispatchId2 } = mgr.start({
|
|
56962
57060
|
sourceSessionId,
|
|
56963
57061
|
targetPersona,
|
|
57062
|
+
...sourcePersonaId ? { sourcePersonaId } : {},
|
|
57063
|
+
// A 端台账记「我派给了谁的机器」:worker 会话在对端,本机点不进去,卡片的归属带
|
|
57064
|
+
// 靠这个字段说出「在哪台机器上」而不是干瞪眼(design §4 跨设备那一行)。
|
|
57065
|
+
targetDeviceId: args.targetDeviceId,
|
|
56964
57066
|
taskText: args.prompt,
|
|
56965
57067
|
...args.brief ? { taskBrief: args.brief } : {},
|
|
56966
57068
|
...args.meta ? { meta: args.meta } : {}
|
|
@@ -57034,6 +57136,9 @@ function buildPersonaDispatchHandlers(deps) {
|
|
|
57034
57136
|
const { dispatchId } = mgr.start({
|
|
57035
57137
|
sourceSessionId,
|
|
57036
57138
|
targetPersona,
|
|
57139
|
+
// 「这活谁派的」的真源。schema 里早就有这个字段,本次之前没有任何写入点——
|
|
57140
|
+
// 于是 worker 会话的 dispatch 卡片永远查不到派活方(归属带只能退回信封)。
|
|
57141
|
+
...sourcePersonaId ? { sourcePersonaId } : {},
|
|
57037
57142
|
taskText: args.prompt,
|
|
57038
57143
|
...args.brief ? { taskBrief: args.brief } : {},
|
|
57039
57144
|
...args.meta ? { meta: args.meta } : {}
|
|
@@ -57100,9 +57205,10 @@ function buildPersonaDispatchHandlers(deps) {
|
|
|
57100
57205
|
const list = async (frame) => {
|
|
57101
57206
|
const { type: _t, requestId: _r, ...rest } = frame;
|
|
57102
57207
|
const args = DispatchListArgsSchema.parse(rest);
|
|
57103
|
-
const records = deps.store.list(
|
|
57104
|
-
args.sourceSessionId ? { sourceSessionId: args.sourceSessionId } :
|
|
57105
|
-
|
|
57208
|
+
const records = deps.store.list({
|
|
57209
|
+
...args.sourceSessionId ? { sourceSessionId: args.sourceSessionId } : {},
|
|
57210
|
+
...args.relatedSessionId ? { relatedSessionId: args.relatedSessionId } : {}
|
|
57211
|
+
});
|
|
57106
57212
|
return {
|
|
57107
57213
|
response: { type: "personaDispatch:list:ok", records }
|
|
57108
57214
|
};
|
|
@@ -65428,7 +65534,7 @@ function computeMethodAccess(args) {
|
|
|
65428
65534
|
}
|
|
65429
65535
|
|
|
65430
65536
|
// src/version.ts
|
|
65431
|
-
var version = "0.2.
|
|
65537
|
+
var version = "0.2.490".length > 0 ? "0.2.490" : "dev";
|
|
65432
65538
|
|
|
65433
65539
|
// src/cli-probe/probe.ts
|
|
65434
65540
|
var fs72 = __toESM(require("fs"), 1);
|
|
@@ -70546,6 +70652,9 @@ async function startDaemon(config) {
|
|
|
70546
70652
|
},
|
|
70547
70653
|
// 续派(spec 2026-07-21):任务包注入 worker session,复用投递同款注入路径。
|
|
70548
70654
|
sendFollowUp: (workerSessionId, text, meta) => manager.injectIntoSession(workerSessionId, text, meta),
|
|
70655
|
+
// 「这活是谁派的」:台账 sourcePersonaId + 续派包信封 from 都吃它。与首发任务包在
|
|
70656
|
+
// SessionManager 那侧的反查(buildDispatchTaskPack 的 from)是同一个字段、同一个查法。
|
|
70657
|
+
getSessionPersonaId: (sessionId) => manager.findOwnedSession(sessionId)?.ownerPersonaId,
|
|
70549
70658
|
logger,
|
|
70550
70659
|
spawnB: async (args) => {
|
|
70551
70660
|
logger.info("dispatch.spawnB.start", {
|
|
@@ -21310,10 +21310,22 @@ var EnvSecretSchema = external_exports.object({
|
|
|
21310
21310
|
var EnvAssetSchema = external_exports.object({
|
|
21311
21311
|
name: external_exports.string().min(1)
|
|
21312
21312
|
});
|
|
21313
|
+
var ConnectorNameSchema = external_exports.string().regex(/^[a-z0-9-]+$/, "\u8FDE\u63A5\u5668\u540D\u53EA\u80FD\u662F\u5C0F\u5199\u5B57\u6BCD\u3001\u6570\u5B57\u548C\u77ED\u6A2A\u7EBF");
|
|
21314
|
+
var EnvConnectorSchema = external_exports.object({ name: ConnectorNameSchema });
|
|
21313
21315
|
var MANIFEST_BODY = {
|
|
21314
21316
|
clis: external_exports.array(EnvCliSchema),
|
|
21315
21317
|
secrets: external_exports.array(EnvSecretSchema),
|
|
21316
|
-
assets: external_exports.array(EnvAssetSchema)
|
|
21318
|
+
assets: external_exports.array(EnvAssetSchema),
|
|
21319
|
+
/**
|
|
21320
|
+
* 装了哪些连接器(design 2026-09-09-cloud-connector-credentials §7.1)。**字符串数组**,
|
|
21321
|
+
* 是四段里唯一不是 `{name}` 对象数组的一段:连接器没有任何附加属性,包一层对象只是多一层壳。
|
|
21322
|
+
*
|
|
21323
|
+
* 不复用 secrets:那段说的是「需要一份值,由部署者填」,这里没有值要填;
|
|
21324
|
+
* 不从 clis 推断:lark-cli 在底座里,每个 persona 都有它,推不出任何东西。
|
|
21325
|
+
*
|
|
21326
|
+
* **缺省空而不是必填**:本机上已有的单据不用为此重扫一遍;没声明就是没装,扫描器照样会报。
|
|
21327
|
+
*/
|
|
21328
|
+
connectors: external_exports.array(ConnectorNameSchema).default([])
|
|
21317
21329
|
};
|
|
21318
21330
|
var EnvManifestBodySchema = external_exports.object(MANIFEST_BODY);
|
|
21319
21331
|
var EnvironmentManifestSchema = external_exports.object({
|
|
@@ -21446,8 +21458,9 @@ var DeployRerunJobArgsSchema = external_exports.object({
|
|
|
21446
21458
|
}).strict();
|
|
21447
21459
|
var DeployReportFindingArgsSchema = external_exports.object({
|
|
21448
21460
|
personaId: external_exports.string().min(1),
|
|
21449
|
-
kind: external_exports.enum(["cli", "secret", "asset"]),
|
|
21461
|
+
kind: external_exports.enum(["cli", "secret", "asset", "connector"]),
|
|
21450
21462
|
// 该条目在 manifest 对应数组里的内容(形状随 kind 变,handler 按 kind 收窄校验)
|
|
21463
|
+
// connector 的 item 只有 name,落进清单时只留名字(manifest.connectors 是字符串数组)
|
|
21451
21464
|
item: external_exports.record(external_exports.string(), external_exports.unknown()),
|
|
21452
21465
|
// 这条发现的来历(一句人话 + 原文出处),形状同 EnvEvidence,target 由 handler 从 kind+name 派生
|
|
21453
21466
|
evidence: external_exports.object({
|
|
@@ -21743,6 +21756,16 @@ var DispatchRecordSchema = external_exports.object({
|
|
|
21743
21756
|
sourceSessionId: external_exports.string().min(1),
|
|
21744
21757
|
sourcePersonaId: external_exports.string().min(1).optional(),
|
|
21745
21758
|
targetPersonaId: external_exports.string().min(1),
|
|
21759
|
+
/**
|
|
21760
|
+
* 跨设备派活时 B 所在的设备(`DispatchRunArgs.targetDeviceId`),A 端台账记「我派给了谁的机器」。
|
|
21761
|
+
*
|
|
21762
|
+
* **缺省即本机**,与入参侧用 `undefined` 表示本机是同一条口径——不引第二种表示法。
|
|
21763
|
+
* 存量台账行没有这个字段,消费方(dispatch 卡片的归属带)据此只显示 persona、不显示设备。
|
|
21764
|
+
*
|
|
21765
|
+
* B 端自己的台账行**不会**有它:peer-forward 转发时故意 strip 掉(B 据此判定本地执行),
|
|
21766
|
+
* 那条行记的是 B 视角的本地 dispatch。
|
|
21767
|
+
*/
|
|
21768
|
+
targetDeviceId: external_exports.string().min(1).optional(),
|
|
21746
21769
|
/** B 的 sessionId;registerBSession 后回填 */
|
|
21747
21770
|
workerSessionId: external_exports.string().min(1).optional(),
|
|
21748
21771
|
/** 任务摘要;截断(≤500 字符)在 daemon store.add 处保证 */
|
|
@@ -21769,7 +21792,17 @@ var DispatchRecordSchema = external_exports.object({
|
|
|
21769
21792
|
deliveredAt: external_exports.string().min(1).optional()
|
|
21770
21793
|
});
|
|
21771
21794
|
var DispatchListArgsSchema = external_exports.object({
|
|
21772
|
-
sourceSessionId: external_exports.string().min(1).optional()
|
|
21795
|
+
sourceSessionId: external_exports.string().min(1).optional(),
|
|
21796
|
+
/**
|
|
21797
|
+
* 「**这条会话卷入的**所有台账行」:`sourceSessionId === it || workerSessionId === it`。
|
|
21798
|
+
*
|
|
21799
|
+
* 为什么不复用 `sourceSessionId`:一条会话既可能是派活方(在自己这儿看回报卡),也可能是
|
|
21800
|
+
* 干活方(在 worker 会话里看任务包),后者用 sourceSessionId 过滤一条也查不到。UI 的
|
|
21801
|
+
* dispatch 卡片要的正是「跟我这条会话有关的全部 dispatch」,一次调用答完,不必拉全量再本地筛。
|
|
21802
|
+
*
|
|
21803
|
+
* 与 `sourceSessionId` 同时给时两个条件都要满足(AND),但调用方没有这么用的场景。
|
|
21804
|
+
*/
|
|
21805
|
+
relatedSessionId: external_exports.string().min(1).optional()
|
|
21773
21806
|
});
|
|
21774
21807
|
var DispatchListResultSchema = external_exports.object({
|
|
21775
21808
|
records: external_exports.array(DispatchRecordSchema)
|
package/dist/persona-defaults/persona-clawd-butler/.claude/skills/scan-persona-for-deploy/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: scan-persona-for-deploy
|
|
3
|
-
description: 老板要把某个 persona 发布到云上时触发——把这个 persona
|
|
3
|
+
description: 老板要把某个 persona 发布到云上时触发——把这个 persona 读一遍,弄清它挪到一台干净的云上机器要预备什么(命令行工具、凭据、随行资产、连接器),边查边填进它的部署文件。收到形如「扫描 persona-xxx 的部署环境,personaId=persona-xxx」的任务时使用。只负责扫,不负责部署。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# 扫 persona 的上云环境
|
|
@@ -35,7 +35,7 @@ description: 老板要把某个 persona 发布到云上时触发——把这个
|
|
|
35
35
|
```
|
|
36
36
|
call({ method: "deploy:reportFinding", args: {
|
|
37
37
|
personaId: "<任务里给的>",
|
|
38
|
-
kind: "cli" | "secret" | "asset",
|
|
38
|
+
kind: "cli" | "secret" | "asset" | "connector",
|
|
39
39
|
item: { ... }, // 见下面各类的字段
|
|
40
40
|
evidence: { reason: "...", file: "...", quote: "..." }
|
|
41
41
|
}})
|
|
@@ -99,6 +99,18 @@ call({ method: "deploy:reportFinding", args: {
|
|
|
99
99
|
- 判据往往落在「这个动作在云上由谁提供」。persona 在本机走 daemon 提供的 MCP 干的活,
|
|
100
100
|
云上容器里没有 daemon,靠的就是那份随行资产。
|
|
101
101
|
|
|
102
|
+
### kind: "connector" —— 以用户本人身份操作的第三方
|
|
103
|
+
|
|
104
|
+
`item` 字段只有 `name`(小写字母、数字、短横线)。目前能声明的只有 `feishu`。
|
|
105
|
+
|
|
106
|
+
判据:**persona 里出现「以用户本人身份操作某个第三方」的动作**就报——查他的日程、发他的消息、
|
|
107
|
+
读他的文档,这类活在云上要先拿到他本人的授权,授权由云上的连接器完成,不是部署者填一份凭据就能代替的。
|
|
108
|
+
|
|
109
|
+
- 跟 `secret` 的区别:`secret` 是「部署者出一份值,全体使用者共用」;`connector` 是「每个使用者各自授权一次」。
|
|
110
|
+
一个 persona 用 `lark-cli … --as user` 操作飞书,报的是 `connector: feishu`,**不要**再给它报一条飞书的 `secret`。
|
|
111
|
+
- 跟 `cli` 的区别:`lark-cli` 这个工具本身在底座镜像里,不用报 `cli`;报 `connector` 说的是「它要用谁的身份」。
|
|
112
|
+
- `evidence.quote` 引 skill 里带 `--as user` 的那类命令行就够。
|
|
113
|
+
|
|
102
114
|
### evidence —— 每条的来历
|
|
103
115
|
|
|
104
116
|
- `reason`:**一句人话**,说清「为什么需要它 + 你从哪看出来的」,写给不熟悉这个 persona 的人看。
|