nolo-cli 0.1.41 → 0.1.43
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 +27 -12
- package/agent-runtime/agentThread.ts +222 -0
- package/agent-runtime/agentThreadAdmission.ts +74 -0
- package/agent-runtime/dialogWritePlan.ts +42 -0
- package/agent-runtime/hostAdapter.ts +1 -0
- package/agent-runtime/index.ts +43 -0
- package/agent-runtime/localLoop.ts +31 -2
- package/agent-runtime/localToolPolicy.ts +1 -0
- package/agent-runtime/localWorkspaceTools.ts +1258 -78
- package/agent-runtime/noloWorkspaceTools.ts +20 -1
- package/agent-runtime/runtimeToolPolicy.ts +1 -5
- package/agent-runtime/runtimeToolSurface.ts +1 -0
- package/agentAliases.ts +29 -9
- package/agentInternalCommandEntries.ts +11 -3
- package/agentMachineCommands.ts +1 -0
- package/agentRecordCommands.ts +2 -2
- package/agentRecordHelpers.ts +59 -6
- package/agentRunCommand.ts +84 -19
- package/ai/agent/buildSystemPrompt.ts +14 -0
- package/ai/agent/cliExecutor.ts +202 -43
- package/ai/agent/createAgentSchema.ts +2 -2
- package/ai/agent/liveAudioModel.ts +1 -1
- package/ai/agent/streamAgentChatTurn.ts +154 -20
- package/ai/agent/streamAgentChatTurnUtils.ts +63 -0
- package/ai/ai.locale.ts +70 -0
- package/ai/chat/inlineImageUrlsForCustomProvider.ts +7 -2
- package/ai/chat/sendOpenAICompletionsRequest.ts +25 -1
- package/ai/llm/kimi.ts +0 -1
- package/ai/llm/openrouterModels.ts +27 -2
- package/ai/memory/policy.ts +49 -0
- package/ai/memory/queryShared.ts +10 -4
- package/ai/memory/recentRelationshipRecap.ts +4 -0
- package/ai/memory/remember.ts +48 -104
- package/ai/memory/scope.ts +74 -0
- package/ai/memory/types.ts +6 -0
- package/ai/memory/understanding.ts +10 -18
- package/ai/policy/runtimePolicy.ts +95 -23
- package/ai/tools/agent/agentTools.ts +0 -21
- package/ai/tools/agent/startAgentDialogTool.ts +2 -1
- package/ai/tools/index.ts +2 -20
- package/ai/tools/prepareTools.ts +171 -1
- package/ai/tools/readXhsProfileTool.ts +80 -69
- package/ai/tools/table/createTableTool.ts +11 -0
- package/ai/tools/toolPacks.ts +19 -0
- package/ai/types.ts +3 -0
- package/app/favorite/deletedFavoriteProjection.ts +31 -0
- package/app/hooks/deleteDbKey.ts +24 -3
- package/app/i18n/translations/interface.locale.ts +48 -16
- package/app/types/appSummary.ts +4 -0
- package/app/utils/desktopAgentRuntimeTurnClient.ts +2 -0
- package/auth/adminPermissions.ts +64 -4
- package/auth/routes.ts +5 -0
- package/chat/chat.locale.ts +4 -0
- package/chat/dialog/actions/handleSendMessageAction.ts +7 -2
- package/chat/dialog/dialogAttachmentCleanup.ts +135 -0
- package/chat/dialog/dialogSlice.ts +117 -4
- package/chat/messages/activityCompletion.ts +97 -0
- package/chat/messages/imagePayloadPersistence.ts +22 -0
- package/chat/messages/messageContent.ts +20 -16
- package/chat/messages/messageSlice.ts +20 -1
- package/chat/messages/toolThunks.ts +32 -11
- package/chat/messages/types.ts +8 -1
- package/chat/messages/web/toolDisplayName.ts +686 -0
- package/cli/agentAliases.ts +29 -9
- package/cli/agentInternalCommandEntries.ts +11 -3
- package/cli/agentMachineCommands.ts +1 -0
- package/cli/agentRecordCommands.ts +2 -2
- package/cli/agentRecordHelpers.ts +59 -6
- package/cli/agentRunCommand.ts +84 -19
- package/cli/client/agentRun.ts +65 -253
- package/cli/client/localRuntimeAdapter.ts +179 -53
- package/cli/commandRegistry.ts +7 -4
- package/cli/dialogAttachmentCleanup.ts +151 -0
- package/cli/dialogCommands.ts +163 -61
- package/cli/dialogInternalCommandEntries.ts +1 -1
- package/cli/docCreateCommands.ts +345 -0
- package/cli/docInternalCommandEntries.ts +11 -0
- package/cli/docPageHelpers.ts +80 -0
- package/cli/docSkillHelpers.ts +67 -0
- package/cli/docSpaceHelpers.ts +46 -0
- package/cli/internalCommandEntries.ts +4 -0
- package/cli/machineCommands.ts +18 -34
- package/cli/memoryCommands.ts +253 -0
- package/cli/memoryInternalCommandEntries.ts +9 -0
- package/cli/qoderUsageProbe.ts +252 -0
- package/cli/scriptCommandEntries.ts +0 -3
- package/cli/secretScan.ts +31 -0
- package/cli/tableCommands.ts +248 -13
- package/cli/workflowInternalCommandEntries.ts +2 -3
- package/client/agentRun.test.ts +68 -84
- package/client/agentRun.ts +65 -253
- package/client/localRuntimeAdapter.test.ts +505 -74
- package/client/localRuntimeAdapter.ts +179 -53
- package/commandRegistry.ts +7 -4
- package/connector-experimental/capabilities.ts +10 -9
- package/create/space/sidebarVisibleTypes.ts +19 -0
- package/dialogAttachmentCleanup.ts +151 -0
- package/dialogCommands.ts +163 -61
- package/dialogInternalCommandEntries.ts +1 -1
- package/docCreateCommands.ts +345 -0
- package/docInternalCommandEntries.ts +11 -0
- package/docPageHelpers.ts +80 -0
- package/docSkillHelpers.ts +67 -0
- package/docSpaceHelpers.ts +46 -0
- package/integrations/google/models.ts +4 -4
- package/integrations/openai/generateOpenAIRequestBody.ts +0 -3
- package/integrations/xhs-reader/types.ts +36 -3
- package/integrations/xhs-reader/url.ts +24 -1
- package/internalCommandEntries.ts +4 -0
- package/machineCommands.ts +18 -34
- package/memoryCommands.ts +253 -0
- package/memoryInternalCommandEntries.ts +9 -0
- package/package.json +141 -114
- package/qoderUsageProbe.ts +252 -0
- package/render/contentIcon/types.ts +167 -0
- package/render/page/docSlice.ts +30 -3
- package/render/page/types.ts +2 -0
- package/render/table/createTableAction.ts +4 -0
- package/render/table/tableSlice.ts +61 -0
- package/render/table/types.ts +10 -0
- package/scriptCommandEntries.ts +0 -3
- package/secretScan.ts +31 -0
- package/server/handlers/agentRun/types.ts +24 -23
- package/tableCommands.ts +248 -13
- package/workflowInternalCommandEntries.ts +2 -3
- package/ai/tools/agent/taskRunTool.ts +0 -112
- package/cli/client/taskRunPrompt.ts +0 -27
- package/cli/taskRunCommand.ts +0 -265
- package/client/taskRunPrompt.ts +0 -27
- package/integrations/xhs-reader/analyze/profileAnalyzer.ts +0 -205
- package/integrations/xhs-reader/backends/playwrightProfileCollector.ts +0 -624
- package/integrations/xhs-reader/bridge/readXhsProfileWithBridge.ts +0 -227
- package/integrations/xhs-reader/client/xhsWebApiClient.ts +0 -229
- package/integrations/xhs-reader/normalize.ts +0 -390
- package/integrations/xhs-reader/orchestrator.ts +0 -178
- package/taskRunCommand.ts +0 -265
package/cli/taskRunCommand.ts
DELETED
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
import { DEFAULT_NOLO_SERVER_URL } from "./defaultServer";
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
|
|
6
|
-
type EnvLike = Record<string, string | undefined>;
|
|
7
|
-
type OutputLike = { write(chunk: string): unknown };
|
|
8
|
-
|
|
9
|
-
type TaskRunCommandDeps = {
|
|
10
|
-
env?: EnvLike;
|
|
11
|
-
output?: OutputLike;
|
|
12
|
-
fetchImpl?: typeof fetch;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
function parseEnvFile(path: string): EnvLike {
|
|
16
|
-
if (!path || !existsSync(path)) return {};
|
|
17
|
-
const parsed: EnvLike = {};
|
|
18
|
-
for (const rawLine of readFileSync(path, "utf8").split(/\r?\n/)) {
|
|
19
|
-
const line = rawLine.trim();
|
|
20
|
-
if (!line || line.startsWith("#")) continue;
|
|
21
|
-
const index = line.indexOf("=");
|
|
22
|
-
if (index <= 0) continue;
|
|
23
|
-
const key = line.slice(0, index).trim();
|
|
24
|
-
let value = line.slice(index + 1).trim();
|
|
25
|
-
if (
|
|
26
|
-
(value.startsWith('"') && value.endsWith('"')) ||
|
|
27
|
-
(value.startsWith("'") && value.endsWith("'"))
|
|
28
|
-
) {
|
|
29
|
-
value = value.slice(1, -1);
|
|
30
|
-
}
|
|
31
|
-
if (key) parsed[key] = value;
|
|
32
|
-
}
|
|
33
|
-
return parsed;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function loadTaskRunEnv(env: EnvLike, options: { includeDefaultEnvFiles?: boolean } = {}) {
|
|
37
|
-
const envFileCandidates = [
|
|
38
|
-
env.NOLO_TASK_RUN_ENV_FILE,
|
|
39
|
-
env.NOLO_MACHINE_ENV_FILE,
|
|
40
|
-
...(options.includeDefaultEnvFiles
|
|
41
|
-
? ["/etc/nolo/machine-runner.env", join(homedir(), ".nolo", "machine-runner.env")]
|
|
42
|
-
: []),
|
|
43
|
-
].filter((path): path is string => Boolean(path));
|
|
44
|
-
const fileEnv = Object.assign({}, ...envFileCandidates.map(parseEnvFile));
|
|
45
|
-
return { ...fileEnv, ...env };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function readOption(args: string[], flag: string) {
|
|
49
|
-
for (let i = 0; i < args.length; i++) {
|
|
50
|
-
const value = args[i];
|
|
51
|
-
if (value === flag) return args[i + 1] ?? "";
|
|
52
|
-
if (value.startsWith(`${flag}=`)) return value.slice(flag.length + 1);
|
|
53
|
-
}
|
|
54
|
-
return "";
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function csv(raw: string) {
|
|
58
|
-
return raw.split(",").map((item) => item.trim()).filter(Boolean);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function optionalJson(args: string[], flag: string) {
|
|
62
|
-
const value = optional(args, flag);
|
|
63
|
-
if (!value) return undefined;
|
|
64
|
-
try {
|
|
65
|
-
return JSON.parse(value);
|
|
66
|
-
} catch (error) {
|
|
67
|
-
throw new Error(`${flag} must be valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function resolveServerUrl(args: string[], env: EnvLike) {
|
|
72
|
-
return (
|
|
73
|
-
readOption(args, "--server-url") ||
|
|
74
|
-
readOption(args, "--server") ||
|
|
75
|
-
readOption(args, "--base-url") ||
|
|
76
|
-
env.NOLO_SERVER_URL ||
|
|
77
|
-
env.NOLO_SERVER ||
|
|
78
|
-
env.BASE_URL ||
|
|
79
|
-
DEFAULT_NOLO_SERVER_URL
|
|
80
|
-
).replace(/\/+$/, "");
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function resolveAuthToken(args: string[], env: EnvLike) {
|
|
84
|
-
return (
|
|
85
|
-
readOption(args, "--machine-key") ||
|
|
86
|
-
readOption(args, "--token") ||
|
|
87
|
-
env.NOLO_TASK_RUN_TOKEN ||
|
|
88
|
-
env.NOLO_MACHINE_API_KEY ||
|
|
89
|
-
env.AUTH_TOKEN ||
|
|
90
|
-
env.AUTH ||
|
|
91
|
-
""
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function required(args: string[], flag: string) {
|
|
96
|
-
const value = readOption(args, flag).trim();
|
|
97
|
-
if (!value) throw new Error(`${flag} is required`);
|
|
98
|
-
return value;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function optional(args: string[], flag: string) {
|
|
102
|
-
const value = readOption(args, flag).trim();
|
|
103
|
-
return value || undefined;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function buildBody(command: string, args: string[]) {
|
|
107
|
-
const dbKey = required(args, "--row-dbkey");
|
|
108
|
-
if (command === "read-context" || command === "readTaskContext") {
|
|
109
|
-
return { action: "readTaskContext", dbKey };
|
|
110
|
-
}
|
|
111
|
-
if (command === "claim-work-item" || command === "claimWorkItem") {
|
|
112
|
-
return {
|
|
113
|
-
action: "claimWorkItem",
|
|
114
|
-
dbKey,
|
|
115
|
-
workItemId: required(args, "--work-item-id"),
|
|
116
|
-
agentKey: required(args, "--agent-key"),
|
|
117
|
-
role: optional(args, "--role"),
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
if (command === "record-agent-run" || command === "recordAgentRun") {
|
|
121
|
-
const toolTraceSummary = optionalJson(args, "--tool-trace-summary");
|
|
122
|
-
return {
|
|
123
|
-
action: "recordAgentRun",
|
|
124
|
-
dbKey,
|
|
125
|
-
...(optional(args, "--run-id") ? { runId: optional(args, "--run-id") } : {}),
|
|
126
|
-
agentKey: required(args, "--agent-key"),
|
|
127
|
-
...(optional(args, "--role") ? { role: optional(args, "--role") } : {}),
|
|
128
|
-
runStatus: required(args, "--run-status"),
|
|
129
|
-
...(optional(args, "--work-item-id") ? { workItemId: optional(args, "--work-item-id") } : {}),
|
|
130
|
-
...(optional(args, "--dialog-id") ? { dialogId: optional(args, "--dialog-id") } : {}),
|
|
131
|
-
...(optional(args, "--result-summary") ? { resultSummary: optional(args, "--result-summary") } : {}),
|
|
132
|
-
...(optional(args, "--error-code") ? { errorCode: optional(args, "--error-code") } : {}),
|
|
133
|
-
...(optional(args, "--tools-used") ? { toolsUsed: csv(optional(args, "--tools-used")!) } : {}),
|
|
134
|
-
...(toolTraceSummary ? { toolTraceSummary } : {}),
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
if (command === "set-blocker" || command === "setBlocker") {
|
|
138
|
-
return {
|
|
139
|
-
action: "setBlocker",
|
|
140
|
-
dbKey,
|
|
141
|
-
workItemId: optional(args, "--work-item-id"),
|
|
142
|
-
blocker: {
|
|
143
|
-
layer: required(args, "--layer"),
|
|
144
|
-
code: required(args, "--code"),
|
|
145
|
-
message: required(args, "--message"),
|
|
146
|
-
},
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
if (command === "clear-blocker" || command === "clearBlocker") {
|
|
150
|
-
return {
|
|
151
|
-
action: "clearBlocker",
|
|
152
|
-
dbKey,
|
|
153
|
-
blockerId: required(args, "--blocker-id"),
|
|
154
|
-
workItemId: optional(args, "--work-item-id"),
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
if (command === "submit-outcome" || command === "submitOutcome") {
|
|
158
|
-
return {
|
|
159
|
-
action: "submitOutcome",
|
|
160
|
-
dbKey,
|
|
161
|
-
workItemId: required(args, "--work-item-id"),
|
|
162
|
-
summary: required(args, "--summary"),
|
|
163
|
-
...(optional(args, "--dialog-id") ? { dialogId: optional(args, "--dialog-id") } : {}),
|
|
164
|
-
...(optional(args, "--artifact-ids") ? { artifactIds: csv(optional(args, "--artifact-ids")!) } : {}),
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
if (command === "request-review" || command === "requestReview") {
|
|
168
|
-
return {
|
|
169
|
-
action: "requestReview",
|
|
170
|
-
dbKey,
|
|
171
|
-
workItemId: optional(args, "--work-item-id"),
|
|
172
|
-
reviewerAgentKey: required(args, "--reviewer-agent-key"),
|
|
173
|
-
...(optional(args, "--dialog-id") ? { dialogId: optional(args, "--dialog-id") } : {}),
|
|
174
|
-
...(optional(args, "--message") ? { message: optional(args, "--message") } : {}),
|
|
175
|
-
...(optional(args, "--artifact-ids") ? { artifactIds: csv(optional(args, "--artifact-ids")!) } : {}),
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
if (command === "record-review" || command === "recordReview") {
|
|
179
|
-
return {
|
|
180
|
-
action: "recordReview",
|
|
181
|
-
dbKey,
|
|
182
|
-
...(optional(args, "--work-item-id") ? { workItemId: optional(args, "--work-item-id") } : {}),
|
|
183
|
-
reviewStatus: required(args, "--review-status"),
|
|
184
|
-
...(optional(args, "--reviewer-agent-key")
|
|
185
|
-
? { reviewerAgentKey: optional(args, "--reviewer-agent-key") }
|
|
186
|
-
: {}),
|
|
187
|
-
...(optional(args, "--dialog-id") ? { dialogId: optional(args, "--dialog-id") } : {}),
|
|
188
|
-
...(optional(args, "--findings") ? { findings: optional(args, "--findings") } : {}),
|
|
189
|
-
...(optional(args, "--artifact-ids") ? { artifactIds: csv(optional(args, "--artifact-ids")!) } : {}),
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
if (command === "close-work-item" || command === "complete-work-item" || command === "closeWorkItem" || command === "completeWorkItem") {
|
|
193
|
-
return {
|
|
194
|
-
action: "closeWorkItem",
|
|
195
|
-
dbKey,
|
|
196
|
-
workItemId: required(args, "--work-item-id"),
|
|
197
|
-
summary: required(args, "--summary"),
|
|
198
|
-
...(optional(args, "--reason") ? { reason: optional(args, "--reason") } : {}),
|
|
199
|
-
...(optional(args, "--dialog-id") ? { dialogId: optional(args, "--dialog-id") } : {}),
|
|
200
|
-
...(optional(args, "--artifact-ids") ? { artifactIds: csv(optional(args, "--artifact-ids")!) } : {}),
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
throw new Error(`Unknown task-run command: ${command}`);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function usage() {
|
|
207
|
-
return [
|
|
208
|
-
"Usage:",
|
|
209
|
-
" nolo task-run read-context --row-dbkey <row>",
|
|
210
|
-
" nolo task-run claim-work-item --row-dbkey <row> --work-item-id <id> --agent-key <agent> [--role <role>]",
|
|
211
|
-
" nolo task-run record-agent-run --row-dbkey <row> --agent-key <agent> --run-status <status> [--tools-used <csv>]",
|
|
212
|
-
" nolo task-run set-blocker --row-dbkey <row> [--work-item-id <id>] --layer <layer> --code <code> --message <message>",
|
|
213
|
-
" nolo task-run clear-blocker --row-dbkey <row> --blocker-id <id> [--work-item-id <id>]",
|
|
214
|
-
" nolo task-run submit-outcome --row-dbkey <row> --work-item-id <id> --summary <summary> [--dialog-id <id>]",
|
|
215
|
-
" nolo task-run request-review --row-dbkey <row> --reviewer-agent-key <agent> [--work-item-id <id>]",
|
|
216
|
-
" nolo task-run record-review --row-dbkey <row> [--work-item-id <id>] --review-status <passed|needs_changes|blocked> [--findings <text>]",
|
|
217
|
-
" nolo task-run close-work-item --row-dbkey <row> --work-item-id <id> --summary <summary>",
|
|
218
|
-
"",
|
|
219
|
-
].join("\n");
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
export async function runTaskRunCommand(args: string[], deps: TaskRunCommandDeps = {}) {
|
|
223
|
-
const env = loadTaskRunEnv(deps.env ?? process.env, { includeDefaultEnvFiles: deps.env === undefined });
|
|
224
|
-
const output = deps.output ?? process.stdout;
|
|
225
|
-
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
226
|
-
const command = args[0];
|
|
227
|
-
if (!command || command === "--help" || command === "-h") {
|
|
228
|
-
output.write(usage());
|
|
229
|
-
return 0;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
let body: Record<string, unknown>;
|
|
233
|
-
try {
|
|
234
|
-
body = buildBody(command, args.slice(1));
|
|
235
|
-
} catch (error) {
|
|
236
|
-
output.write(`[nolo] task-run failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
237
|
-
return 1;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const authToken = resolveAuthToken(args, env);
|
|
241
|
-
if (!authToken) {
|
|
242
|
-
output.write("[nolo] task-run failed: AUTH_TOKEN or NOLO_MACHINE_API_KEY is required.\n");
|
|
243
|
-
return 1;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const serverUrl = resolveServerUrl(args, env);
|
|
247
|
-
let response: Response;
|
|
248
|
-
try {
|
|
249
|
-
response = await fetchImpl(`${serverUrl}/api/task-run`, {
|
|
250
|
-
method: "POST",
|
|
251
|
-
headers: {
|
|
252
|
-
Authorization: `Bearer ${authToken}`,
|
|
253
|
-
"Content-Type": "application/json",
|
|
254
|
-
},
|
|
255
|
-
body: JSON.stringify(body),
|
|
256
|
-
});
|
|
257
|
-
} catch (error) {
|
|
258
|
-
output.write(`[nolo] task-run failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
259
|
-
return 1;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const text = await response.text();
|
|
263
|
-
output.write(text.trim() ? `${text.trim()}\n` : "");
|
|
264
|
-
return response.ok ? 0 : 1;
|
|
265
|
-
}
|
package/client/taskRunPrompt.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
export type TaskRunPromptContext = {
|
|
2
|
-
rowDbKey: string;
|
|
3
|
-
taskRunId?: string;
|
|
4
|
-
workItemId?: string;
|
|
5
|
-
artifactIds?: string[];
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
export function prependTaskRunPrompt(message: string, context?: TaskRunPromptContext): string {
|
|
9
|
-
if (!context?.rowDbKey) return message;
|
|
10
|
-
const artifactIds = context.artifactIds?.filter(Boolean) ?? [];
|
|
11
|
-
return [
|
|
12
|
-
"AI-native task-run context:",
|
|
13
|
-
"- This system is AI-native. The current task board is only the present observation and validation use case; different users, companies, and workflows may use different task owners later.",
|
|
14
|
-
"- Treat the task row meta as the durable execution context. Do not infer branch, runner, handoff, review, or blocker state from chat history alone.",
|
|
15
|
-
`- rowDbKey: ${context.rowDbKey}`,
|
|
16
|
-
...(context.taskRunId ? [`- taskRunId: ${context.taskRunId}`] : []),
|
|
17
|
-
...(context.workItemId ? [`- workItemId: ${context.workItemId}`] : []),
|
|
18
|
-
...(artifactIds.length ? [`- artifactIds: ${artifactIds.join(",")}`] : []),
|
|
19
|
-
"- Handoff rule: start from the durable task context, then inspect existing refs such as workItems.dialogIds, workItems.outcome, review.dialogId, activityRefs, agentRuns.dialogId/resultSummary, and artifactRefs. Use dialog read/search for exact evidence.",
|
|
20
|
-
"- When you create, update, hand off, block, or review work, update the task context with `bun run task:run -- ...`.",
|
|
21
|
-
"- If this turn already includes a server TaskRun preflight snapshot, use that snapshot directly for read-only reporting or planning.",
|
|
22
|
-
"- Keep long reasoning and logs in the dialog; record only resumable references in task row meta.",
|
|
23
|
-
"",
|
|
24
|
-
"User task:",
|
|
25
|
-
message,
|
|
26
|
-
].join("\n");
|
|
27
|
-
}
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Deterministic profile analyzer for XHS collected data.
|
|
3
|
-
* No LLM calls. Uses keyword-bucket heuristics for comment categorization.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type {
|
|
7
|
-
XhsNoteSummary,
|
|
8
|
-
XhsNoteDetail,
|
|
9
|
-
XhsComment,
|
|
10
|
-
XhsProfileAnalysis,
|
|
11
|
-
XhsTopNote,
|
|
12
|
-
XhsCommentBucket,
|
|
13
|
-
} from "../types";
|
|
14
|
-
|
|
15
|
-
// --- Comment keyword buckets ---
|
|
16
|
-
|
|
17
|
-
const BUCKET_DEFINITIONS: Array<{
|
|
18
|
-
label: string;
|
|
19
|
-
keywords: string[];
|
|
20
|
-
}> = [
|
|
21
|
-
{
|
|
22
|
-
label: "询问求链接/教程",
|
|
23
|
-
keywords: ["链接", "求链接", "怎么买", "在哪买", "哪里买", "怎么弄", "教程", "怎么做的", "求分享", "私", "私信"],
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
label: "建议/攻略",
|
|
27
|
-
keywords: ["建议", "攻略", "推荐", "避雷", "踩雷", "注意", "tips", "经验", "分享", "干货"],
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
label: "赞美/鼓励",
|
|
31
|
-
keywords: ["好看", "太美", "太好看了", "好棒", "真棒", "厉害", "赞", "优秀", "绝了", "太绝", "爱了", "心动", "种草", "好爱"],
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
label: "提问/互动",
|
|
35
|
-
keywords: ["?", "?", "吗", "多少钱", "什么", "怎么", "为什么", "哪个", "什么时候"],
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
label: "搞笑/段子",
|
|
39
|
-
keywords: ["哈哈", "笑死", "哈哈哈", "笑死我", "绝绝子", "笑不活", "救命", "破防"],
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
label: "个人经历",
|
|
43
|
-
keywords: ["我也", "我之前", "我当时", "我是", "我用过", "我去过", "我觉得", "我也买", "我的"],
|
|
44
|
-
},
|
|
45
|
-
];
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Classify a comment into one or more buckets by keyword matching.
|
|
49
|
-
* Returns bucket indices.
|
|
50
|
-
*/
|
|
51
|
-
function classifyComment(content: string): number[] {
|
|
52
|
-
const lower = content.toLowerCase();
|
|
53
|
-
const matched: number[] = [];
|
|
54
|
-
for (let i = 0; i < BUCKET_DEFINITIONS.length; i++) {
|
|
55
|
-
const bucket = BUCKET_DEFINITIONS[i];
|
|
56
|
-
if (bucket.keywords.some((kw) => lower.includes(kw))) {
|
|
57
|
-
matched.push(i);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return matched;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Find the note with the highest value for a given metric accessor.
|
|
65
|
-
*/
|
|
66
|
-
function findTopNote(
|
|
67
|
-
details: XhsNoteDetail[],
|
|
68
|
-
accessor: (d: XhsNoteDetail) => number,
|
|
69
|
-
): XhsTopNote | null {
|
|
70
|
-
if (details.length === 0) return null;
|
|
71
|
-
let best: XhsNoteDetail | null = null;
|
|
72
|
-
let bestValue = -1;
|
|
73
|
-
for (const d of details) {
|
|
74
|
-
const v = accessor(d);
|
|
75
|
-
if (v > bestValue) {
|
|
76
|
-
bestValue = v;
|
|
77
|
-
best = d;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
if (!best) return null;
|
|
81
|
-
return {
|
|
82
|
-
noteId: best.noteId,
|
|
83
|
-
title: best.title,
|
|
84
|
-
count: bestValue,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Analyze a collection of normalized notes and comments.
|
|
90
|
-
*
|
|
91
|
-
* @param notes - Note summaries (from profile listing).
|
|
92
|
-
* @param details - Note details (if enriched).
|
|
93
|
-
* @param commentsByNote - Map of noteId -> top-level comments.
|
|
94
|
-
*/
|
|
95
|
-
export function analyzeProfile(
|
|
96
|
-
notes: XhsNoteSummary[],
|
|
97
|
-
details: XhsNoteDetail[],
|
|
98
|
-
commentsByNote: Record<string, XhsComment[]>,
|
|
99
|
-
): XhsProfileAnalysis {
|
|
100
|
-
// Use details when available, fall back to note summaries
|
|
101
|
-
const hasDetails = details.length > 0;
|
|
102
|
-
|
|
103
|
-
const highestLikedNote = hasDetails
|
|
104
|
-
? findTopNote(details, (d) => d.metrics.likedCount)
|
|
105
|
-
: findTopNoteFromSummaries(notes, (n) => n.likedCount ?? 0);
|
|
106
|
-
|
|
107
|
-
const highestCommentedNote = hasDetails
|
|
108
|
-
? findTopNote(details, (d) => d.metrics.commentCount)
|
|
109
|
-
: findTopNoteFromCollectedComments(notes, commentsByNote);
|
|
110
|
-
|
|
111
|
-
const highestCollectedNote = hasDetails
|
|
112
|
-
? findTopNote(details, (d) => d.metrics.collectedCount)
|
|
113
|
-
: null;
|
|
114
|
-
|
|
115
|
-
const highestSharedNote = hasDetails
|
|
116
|
-
? findTopNote(details, (d) => d.metrics.shareCount)
|
|
117
|
-
: null;
|
|
118
|
-
|
|
119
|
-
// Aggregate all comments
|
|
120
|
-
const allComments: XhsComment[] = [];
|
|
121
|
-
for (const noteId of Object.keys(commentsByNote)) {
|
|
122
|
-
allComments.push(...commentsByNote[noteId]);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Classify comments into buckets
|
|
126
|
-
const bucketCounts = BUCKET_DEFINITIONS.map(() => 0);
|
|
127
|
-
const bucketSamples = BUCKET_DEFINITIONS.map((): string[] => []);
|
|
128
|
-
|
|
129
|
-
for (const comment of allComments) {
|
|
130
|
-
const indices = classifyComment(comment.content);
|
|
131
|
-
for (const idx of indices) {
|
|
132
|
-
bucketCounts[idx]++;
|
|
133
|
-
if (bucketSamples[idx].length < 3) {
|
|
134
|
-
bucketSamples[idx].push(comment.commentId);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const commentBuckets: XhsCommentBucket[] = BUCKET_DEFINITIONS.map(
|
|
140
|
-
(def, i) => ({
|
|
141
|
-
label: def.label,
|
|
142
|
-
count: bucketCounts[i],
|
|
143
|
-
sampleCommentIds: bucketSamples[i],
|
|
144
|
-
}),
|
|
145
|
-
).filter((b) => b.count > 0);
|
|
146
|
-
|
|
147
|
-
// Top liked comments
|
|
148
|
-
const topLikedComments = [...allComments]
|
|
149
|
-
.sort((a, b) => b.likeCount - a.likeCount)
|
|
150
|
-
.slice(0, 10);
|
|
151
|
-
|
|
152
|
-
return {
|
|
153
|
-
totalNotes: notes.length,
|
|
154
|
-
highestLikedNote,
|
|
155
|
-
highestCommentedNote,
|
|
156
|
-
highestCollectedNote,
|
|
157
|
-
highestSharedNote,
|
|
158
|
-
commentBuckets,
|
|
159
|
-
topLikedComments,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function findTopNoteFromSummaries(
|
|
164
|
-
notes: XhsNoteSummary[],
|
|
165
|
-
accessor: (n: XhsNoteSummary) => number,
|
|
166
|
-
): XhsTopNote | null {
|
|
167
|
-
if (notes.length === 0) return null;
|
|
168
|
-
let best: XhsNoteSummary | null = null;
|
|
169
|
-
let bestValue = -1;
|
|
170
|
-
for (const n of notes) {
|
|
171
|
-
const v = accessor(n);
|
|
172
|
-
if (v > bestValue) {
|
|
173
|
-
bestValue = v;
|
|
174
|
-
best = n;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
if (!best) return null;
|
|
178
|
-
return {
|
|
179
|
-
noteId: best.noteId,
|
|
180
|
-
title: best.title,
|
|
181
|
-
count: bestValue,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function findTopNoteFromCollectedComments(
|
|
186
|
-
notes: XhsNoteSummary[],
|
|
187
|
-
commentsByNote: Record<string, XhsComment[]>,
|
|
188
|
-
): XhsTopNote | null {
|
|
189
|
-
if (notes.length === 0) return null;
|
|
190
|
-
let best: XhsNoteSummary | null = null;
|
|
191
|
-
let bestValue = -1;
|
|
192
|
-
for (const note of notes) {
|
|
193
|
-
const value = commentsByNote[note.noteId]?.length ?? 0;
|
|
194
|
-
if (value > bestValue) {
|
|
195
|
-
bestValue = value;
|
|
196
|
-
best = note;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
if (!best || bestValue <= 0) return null;
|
|
200
|
-
return {
|
|
201
|
-
noteId: best.noteId,
|
|
202
|
-
title: best.title,
|
|
203
|
-
count: bestValue,
|
|
204
|
-
};
|
|
205
|
-
}
|