ai-worklens-agent 0.1.0 → 0.1.1
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/package.json +1 -1
- package/src/config.mjs +4 -4
- package/src/event-builder.mjs +1 -1
- package/src/hook-smoke.mjs +1 -1
- package/src/install.mjs +16 -7
- package/src/protocol/client-update-policy.mjs +49 -0
- package/src/protocol/collection-settings.mjs +41 -0
- package/src/protocol/event-schema.mjs +177 -0
- package/src/protocol/event-types.mjs +61 -0
- package/src/protocol/model-info.mjs +85 -0
- package/src/protocol/pinyin.mjs +181 -0
- package/src/protocol/tool-profiles.mjs +62 -0
- package/src/team-rollout.mjs +3 -3
- package/src/uploader.mjs +2 -2
package/package.json
CHANGED
package/src/config.mjs
CHANGED
|
@@ -2,10 +2,10 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import crypto from "node:crypto";
|
|
5
|
-
import { normalizeCollectionSettings } from "
|
|
6
|
-
import { normalizeClientUpdatePolicy } from "
|
|
7
|
-
import { normalizeModelInfo } from "
|
|
8
|
-
import { normalizeToolId } from "
|
|
5
|
+
import { normalizeCollectionSettings } from "./protocol/collection-settings.mjs";
|
|
6
|
+
import { normalizeClientUpdatePolicy } from "./protocol/client-update-policy.mjs";
|
|
7
|
+
import { normalizeModelInfo } from "./protocol/model-info.mjs";
|
|
8
|
+
import { normalizeToolId } from "./protocol/tool-profiles.mjs";
|
|
9
9
|
|
|
10
10
|
const DEFAULT_CONFIG_DIR = ".ai-worklens";
|
|
11
11
|
|
package/src/event-builder.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { normalizeCollectorEvent, stableHash } from "
|
|
2
|
+
import { normalizeCollectorEvent, stableHash } from "./protocol/event-schema.mjs";
|
|
3
3
|
|
|
4
4
|
function parseList(value) {
|
|
5
5
|
if (!value) return [];
|
package/src/hook-smoke.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { loadClientConfig } from "./config.mjs";
|
|
5
5
|
import { normalizeHookPayload } from "./hook-adapter.mjs";
|
|
6
|
-
import { normalizeToolId } from "
|
|
6
|
+
import { normalizeToolId } from "./protocol/tool-profiles.mjs";
|
|
7
7
|
|
|
8
8
|
const SAMPLE_PAYLOADS = {
|
|
9
9
|
codex: {
|
package/src/install.mjs
CHANGED
|
@@ -4,10 +4,10 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { writeClientConfig } from "./config.mjs";
|
|
7
|
-
import { getToolProfile, listToolProfiles, normalizeToolId } from "
|
|
7
|
+
import { getToolProfile, listToolProfiles, normalizeToolId } from "./protocol/tool-profiles.mjs";
|
|
8
8
|
|
|
9
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
const
|
|
10
|
+
const agentSrcDir = __dirname;
|
|
11
11
|
|
|
12
12
|
function parseArgs(argv) {
|
|
13
13
|
const result = {};
|
|
@@ -32,9 +32,9 @@ export function buildInstallManifest(targetDir, options = {}) {
|
|
|
32
32
|
const displayTargetDir = displayPath(targetDir);
|
|
33
33
|
const configFile = path.join(displayTargetDir, "client.json");
|
|
34
34
|
const mcpCommand = node;
|
|
35
|
-
const mcpArgs = [path.join(
|
|
35
|
+
const mcpArgs = [path.join(agentSrcDir, "mcp-server.mjs")];
|
|
36
36
|
const hookCommand = node;
|
|
37
|
-
const hookArgs = [path.join(
|
|
37
|
+
const hookArgs = [path.join(agentSrcDir, "hook-adapter.mjs"), "--config", configFile, "--tool", selectedTool];
|
|
38
38
|
const artifacts = buildToolArtifacts({
|
|
39
39
|
targetDir,
|
|
40
40
|
displayTargetDir,
|
|
@@ -64,7 +64,7 @@ export function buildInstallManifest(targetDir, options = {}) {
|
|
|
64
64
|
},
|
|
65
65
|
eventCli: {
|
|
66
66
|
command: node,
|
|
67
|
-
args: [path.join(
|
|
67
|
+
args: [path.join(agentSrcDir, "cli.mjs"), "--config", configFile]
|
|
68
68
|
},
|
|
69
69
|
generatedFiles: {
|
|
70
70
|
clientConfig: configFile,
|
|
@@ -202,7 +202,7 @@ function upperFirst(value) {
|
|
|
202
202
|
|
|
203
203
|
function buildToolArtifacts({ targetDir, displayTargetDir, configFile, mcpCommand, mcpArgs }) {
|
|
204
204
|
return Object.fromEntries(listToolProfiles().map((profile) => {
|
|
205
|
-
const hookArgs = [path.join(
|
|
205
|
+
const hookArgs = [path.join(agentSrcDir, "hook-adapter.mjs"), "--config", configFile, "--tool", profile.id];
|
|
206
206
|
const artifact = {
|
|
207
207
|
profile,
|
|
208
208
|
configFile: path.join(targetDir, profile.configFileName),
|
|
@@ -580,7 +580,16 @@ function buildReadme(manifest) {
|
|
|
580
580
|
].join("\n") + "\n";
|
|
581
581
|
}
|
|
582
582
|
|
|
583
|
-
|
|
583
|
+
function isMainModule() {
|
|
584
|
+
if (!process.argv[1]) return false;
|
|
585
|
+
try {
|
|
586
|
+
return fileURLToPath(import.meta.url) === fs.realpathSync(process.argv[1]);
|
|
587
|
+
} catch {
|
|
588
|
+
return fileURLToPath(import.meta.url) === path.resolve(process.argv[1] || "");
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (isMainModule()) {
|
|
584
593
|
const args = parseArgs(process.argv.slice(2));
|
|
585
594
|
const result = installClient({
|
|
586
595
|
targetDir: args["target-dir"],
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export const CLIENT_AGENT_VERSION = "0.1.0";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_CLIENT_UPDATE_POLICY = {
|
|
4
|
+
enabled: true,
|
|
5
|
+
autoUpdate: true,
|
|
6
|
+
clientVersion: CLIENT_AGENT_VERSION,
|
|
7
|
+
artifactRevision: "0.1.0",
|
|
8
|
+
checkIntervalMinutes: 360,
|
|
9
|
+
registerBackgroundTask: true,
|
|
10
|
+
refreshArtifacts: true
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function normalizeClientUpdatePolicy(input = {}, fallback = DEFAULT_CLIENT_UPDATE_POLICY) {
|
|
14
|
+
const current = { ...DEFAULT_CLIENT_UPDATE_POLICY, ...(fallback || {}) };
|
|
15
|
+
const version = String(input.clientVersion || input.desiredClientVersion || current.clientVersion || CLIENT_AGENT_VERSION).trim();
|
|
16
|
+
const revision = String(input.artifactRevision || current.artifactRevision || version).trim();
|
|
17
|
+
return {
|
|
18
|
+
...current,
|
|
19
|
+
enabled: input.enabled !== undefined ? normalizeBoolean(input.enabled) : current.enabled,
|
|
20
|
+
autoUpdate: input.autoUpdate !== undefined ? normalizeBoolean(input.autoUpdate) : current.autoUpdate,
|
|
21
|
+
clientVersion: version || CLIENT_AGENT_VERSION,
|
|
22
|
+
artifactRevision: revision || version || CLIENT_AGENT_VERSION,
|
|
23
|
+
checkIntervalMinutes: clampNumber(input.checkIntervalMinutes, 15, 10080, current.checkIntervalMinutes),
|
|
24
|
+
registerBackgroundTask: input.registerBackgroundTask !== undefined ? normalizeBoolean(input.registerBackgroundTask) : current.registerBackgroundTask,
|
|
25
|
+
refreshArtifacts: input.refreshArtifacts !== undefined ? normalizeBoolean(input.refreshArtifacts) : current.refreshArtifacts,
|
|
26
|
+
appliedRevision: input.appliedRevision ? String(input.appliedRevision) : current.appliedRevision || "",
|
|
27
|
+
lastUpdatedAt: input.lastUpdatedAt ? String(input.lastUpdatedAt) : current.lastUpdatedAt || ""
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function updateNeeded(configVersion, configUpdate = {}, policy = DEFAULT_CLIENT_UPDATE_POLICY) {
|
|
32
|
+
const normalized = normalizeClientUpdatePolicy(policy);
|
|
33
|
+
if (!normalized.enabled || !normalized.autoUpdate) return false;
|
|
34
|
+
if (String(configVersion || "") !== normalized.clientVersion) return true;
|
|
35
|
+
if (normalized.refreshArtifacts && String(configUpdate.appliedRevision || "") !== normalized.artifactRevision) return true;
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeBoolean(value) {
|
|
40
|
+
if (typeof value === "boolean") return value;
|
|
41
|
+
if (typeof value === "string") return !["false", "0", "off", "no"].includes(value.trim().toLowerCase());
|
|
42
|
+
return Boolean(value);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function clampNumber(value, min, max, fallback) {
|
|
46
|
+
const number = Number(value);
|
|
47
|
+
if (!Number.isFinite(number)) return fallback;
|
|
48
|
+
return Math.min(max, Math.max(min, Math.round(number)));
|
|
49
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const DEFAULT_COLLECTION_SETTINGS = {
|
|
2
|
+
enabled: true,
|
|
3
|
+
idleCloseMinutes: 30,
|
|
4
|
+
uploadBatchSize: 50,
|
|
5
|
+
queueWarnThreshold: 10,
|
|
6
|
+
storeRawPrompts: false,
|
|
7
|
+
storeFullReplies: false,
|
|
8
|
+
commandCaptureMode: "summary_only",
|
|
9
|
+
filePathMode: "relative",
|
|
10
|
+
weeklyReportDay: "Monday",
|
|
11
|
+
retentionDays: 180
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function normalizeCollectionSettings(input = {}, fallback = DEFAULT_COLLECTION_SETTINGS) {
|
|
15
|
+
const current = { ...DEFAULT_COLLECTION_SETTINGS, ...(fallback || {}) };
|
|
16
|
+
return {
|
|
17
|
+
...current,
|
|
18
|
+
enabled: input.enabled !== undefined ? normalizeBoolean(input.enabled) : current.enabled,
|
|
19
|
+
idleCloseMinutes: clampNumber(input.idleCloseMinutes, 5, 240, current.idleCloseMinutes),
|
|
20
|
+
uploadBatchSize: clampNumber(input.uploadBatchSize, 1, 500, current.uploadBatchSize),
|
|
21
|
+
queueWarnThreshold: clampNumber(input.queueWarnThreshold, 1, 10000, current.queueWarnThreshold),
|
|
22
|
+
storeRawPrompts: input.storeRawPrompts !== undefined ? normalizeBoolean(input.storeRawPrompts) : current.storeRawPrompts,
|
|
23
|
+
storeFullReplies: input.storeFullReplies !== undefined ? normalizeBoolean(input.storeFullReplies) : current.storeFullReplies,
|
|
24
|
+
commandCaptureMode: ["summary_only", "command_only", "disabled"].includes(input.commandCaptureMode) ? input.commandCaptureMode : current.commandCaptureMode,
|
|
25
|
+
filePathMode: ["relative", "hash", "disabled"].includes(input.filePathMode) ? input.filePathMode : current.filePathMode,
|
|
26
|
+
weeklyReportDay: ["Monday", "Friday", "Sunday"].includes(input.weeklyReportDay) ? input.weeklyReportDay : current.weeklyReportDay,
|
|
27
|
+
retentionDays: clampNumber(input.retentionDays, 7, 3650, current.retentionDays)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeBoolean(value) {
|
|
32
|
+
if (typeof value === "boolean") return value;
|
|
33
|
+
if (typeof value === "string") return !["false", "0", "off", "no"].includes(value.trim().toLowerCase());
|
|
34
|
+
return Boolean(value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function clampNumber(value, min, max, fallback) {
|
|
38
|
+
const number = Number(value);
|
|
39
|
+
if (!Number.isFinite(number)) return fallback;
|
|
40
|
+
return Math.min(max, Math.max(min, Math.round(number)));
|
|
41
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { normalizeModelInfo } from "./model-info.mjs";
|
|
3
|
+
import { EVENT_TYPES } from "./event-types.mjs";
|
|
4
|
+
|
|
5
|
+
const SECRET_PATTERNS = [
|
|
6
|
+
/\b(?:sk|ak|pk)_[A-Za-z0-9_-]{16,}\b/g,
|
|
7
|
+
/\b(?:Bearer\s+)[A-Za-z0-9._-]{16,}\b/gi,
|
|
8
|
+
/\b(?:password|passwd|secret|token|accesskey|cookie)\s*[:=]\s*['"]?[^'"\s]+/gi,
|
|
9
|
+
/\b1[3-9]\d{9}\b/g,
|
|
10
|
+
/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export function stableHash(value) {
|
|
14
|
+
return crypto.createHash("sha256").update(String(value ?? "")).digest("hex").slice(0, 16);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function redactText(value) {
|
|
18
|
+
const input = String(value ?? "");
|
|
19
|
+
let hitCount = 0;
|
|
20
|
+
let output = input;
|
|
21
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
22
|
+
output = output.replace(pattern, () => {
|
|
23
|
+
hitCount += 1;
|
|
24
|
+
return "[REDACTED]";
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return { text: output, hitCount };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function scorePromptQuality(text, hints = {}) {
|
|
31
|
+
const source = String(text ?? "").trim();
|
|
32
|
+
const lower = source.toLowerCase();
|
|
33
|
+
let score = 30;
|
|
34
|
+
const tags = [];
|
|
35
|
+
|
|
36
|
+
if (source.length >= 20) score += 10;
|
|
37
|
+
else tags.push("描述过短");
|
|
38
|
+
if (source.length >= 80) score += 10;
|
|
39
|
+
if (/[。;;,.,\n]/.test(source)) score += 5;
|
|
40
|
+
if (/目标|希望|需要|实现|修复|分析|输出|生成|整理/.test(source)) score += 15;
|
|
41
|
+
else tags.push("目标不明确");
|
|
42
|
+
if (/不要|只|必须|边界|范围|限制|保留|不需要/.test(source)) score += 10;
|
|
43
|
+
else tags.push("缺少边界");
|
|
44
|
+
if (/验收|测试|验证|运行|通过|检查|对比|截图/.test(source)) score += 10;
|
|
45
|
+
else tags.push("缺少验收标准");
|
|
46
|
+
if (hints.hasFiles || /文件|路径|目录|页面|接口|模块|项目/.test(source)) score += 10;
|
|
47
|
+
else tags.push("缺少上下文对象");
|
|
48
|
+
if (lower.includes("随便") || lower.includes("看着办")) {
|
|
49
|
+
score -= 10;
|
|
50
|
+
tags.push("约束过弱");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
score: Math.max(0, Math.min(100, score)),
|
|
55
|
+
tags
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function stringValue(...values) {
|
|
60
|
+
for (const value of values) {
|
|
61
|
+
if (value === undefined || value === null || value === "") continue;
|
|
62
|
+
return String(value);
|
|
63
|
+
}
|
|
64
|
+
return "";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function numberValue(...values) {
|
|
68
|
+
for (const value of values) {
|
|
69
|
+
if (value === undefined || value === null || value === "") continue;
|
|
70
|
+
const number = Number(value);
|
|
71
|
+
if (Number.isFinite(number)) return number;
|
|
72
|
+
}
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normalizeProcess(input, eventType) {
|
|
77
|
+
const metadata = input.metadata && typeof input.metadata === "object" ? input.metadata : {};
|
|
78
|
+
const process = input.process && typeof input.process === "object" ? input.process : {};
|
|
79
|
+
const mode = stringValue(
|
|
80
|
+
process.codexMode,
|
|
81
|
+
process.mode,
|
|
82
|
+
input.codexMode,
|
|
83
|
+
input.codex_mode,
|
|
84
|
+
input.mode,
|
|
85
|
+
metadata.codexMode,
|
|
86
|
+
metadata.mode
|
|
87
|
+
);
|
|
88
|
+
const nextMode = stringValue(
|
|
89
|
+
process.nextMode,
|
|
90
|
+
process.modeTo,
|
|
91
|
+
input.nextMode,
|
|
92
|
+
input.next_mode,
|
|
93
|
+
input.toMode,
|
|
94
|
+
input.to_mode,
|
|
95
|
+
input.modeTo,
|
|
96
|
+
input.mode_to
|
|
97
|
+
);
|
|
98
|
+
return {
|
|
99
|
+
interactionType: stringValue(process.interactionType, input.interactionType, input.interaction_type, metadata.interactionType, eventType),
|
|
100
|
+
codexMode: nextMode || mode,
|
|
101
|
+
previousMode: stringValue(process.previousMode, process.modeFrom, input.previousMode, input.previous_mode, input.fromMode, input.from_mode, input.modeFrom, input.mode_from),
|
|
102
|
+
nextMode,
|
|
103
|
+
phase: stringValue(process.phase, input.phase, metadata.phase),
|
|
104
|
+
roundIndex: numberValue(process.roundIndex, input.roundIndex, input.round_index, metadata.roundIndex),
|
|
105
|
+
toolName: stringValue(process.toolName, input.toolName, input.tool_name, metadata.toolName),
|
|
106
|
+
toolStatus: stringValue(process.toolStatus, input.toolStatus, input.tool_status, input.status, metadata.toolStatus, metadata.status),
|
|
107
|
+
exitCode: numberValue(process.exitCode, input.exitCode, input.exit_code, metadata.exitCode),
|
|
108
|
+
permissionDecision: stringValue(process.permissionDecision, input.permissionDecision, input.permission_decision, input.decision, metadata.permissionDecision),
|
|
109
|
+
retryAttempt: numberValue(process.retryAttempt, input.retryAttempt, input.retry_attempt, metadata.retryAttempt),
|
|
110
|
+
failureKind: stringValue(process.failureKind, input.failureKind, input.failure_kind, input.errorCode, input.error_code, metadata.failureKind)
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function normalizeCollectorEvent(input = {}) {
|
|
115
|
+
const occurredAt = input.occurredAt ? new Date(input.occurredAt) : new Date();
|
|
116
|
+
if (Number.isNaN(occurredAt.getTime())) {
|
|
117
|
+
throw new Error("occurredAt is invalid");
|
|
118
|
+
}
|
|
119
|
+
const eventType = EVENT_TYPES.has(input.eventType) ? input.eventType : "tool_call";
|
|
120
|
+
const summaryTitle = input.summary?.title || input.title || eventType;
|
|
121
|
+
const summaryContent = input.summary?.content || input.summary || "";
|
|
122
|
+
const redactedSummary = redactText(summaryContent);
|
|
123
|
+
const redactedTitle = redactText(summaryTitle);
|
|
124
|
+
const promptMetrics = eventType === "user_prompt"
|
|
125
|
+
? scorePromptQuality(redactedSummary.text || redactedTitle.text, {
|
|
126
|
+
hasFiles: Boolean(input.refs?.files?.length)
|
|
127
|
+
})
|
|
128
|
+
: null;
|
|
129
|
+
|
|
130
|
+
const process = normalizeProcess(input, eventType);
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
schemaVersion: input.schemaVersion || "0.1",
|
|
134
|
+
eventId: input.eventId || `evt_${crypto.randomUUID()}`,
|
|
135
|
+
employeeId: String(input.employeeId || "unknown"),
|
|
136
|
+
employeeName: input.employeeName || "",
|
|
137
|
+
department: input.department || "",
|
|
138
|
+
role: input.role || "",
|
|
139
|
+
clientId: input.clientId || "unknown-client",
|
|
140
|
+
tool: input.tool || "codex",
|
|
141
|
+
source: input.source || "client_agent",
|
|
142
|
+
model: normalizeModelInfo(input),
|
|
143
|
+
eventType,
|
|
144
|
+
occurredAt: occurredAt.toISOString(),
|
|
145
|
+
workspace: {
|
|
146
|
+
rootHash: input.workspace?.rootHash || stableHash(input.workspace?.root || input.workspace?.repoName || "unknown"),
|
|
147
|
+
repoName: input.workspace?.repoName || "unknown",
|
|
148
|
+
branch: input.workspace?.branch || ""
|
|
149
|
+
},
|
|
150
|
+
session: {
|
|
151
|
+
localSessionId: input.session?.localSessionId || input.localSessionId || `auto_${stableHash(`${input.employeeId}:${input.workspace?.repoName}:${occurredAt.toISOString().slice(0, 13)}`)}`,
|
|
152
|
+
turnIndex: Number(input.session?.turnIndex || input.turnIndex || 0),
|
|
153
|
+
idleClosed: Boolean(input.session?.idleClosed)
|
|
154
|
+
},
|
|
155
|
+
summary: {
|
|
156
|
+
title: redactedTitle.text,
|
|
157
|
+
content: redactedSummary.text
|
|
158
|
+
},
|
|
159
|
+
metrics: {
|
|
160
|
+
durationSeconds: Math.max(0, Number(input.metrics?.durationSeconds ?? input.durationSeconds ?? 0)),
|
|
161
|
+
inputChars: Number(input.metrics?.inputChars ?? redactedSummary.text.length),
|
|
162
|
+
promptQualityScore: Number(input.metrics?.promptQualityScore ?? promptMetrics?.score ?? 0)
|
|
163
|
+
},
|
|
164
|
+
process,
|
|
165
|
+
promptQuality: promptMetrics,
|
|
166
|
+
refs: {
|
|
167
|
+
files: Array.isArray(input.refs?.files) ? input.refs.files.slice(0, 20).map(String) : [],
|
|
168
|
+
commands: Array.isArray(input.refs?.commands) ? input.refs.commands.slice(0, 10).map(String) : []
|
|
169
|
+
},
|
|
170
|
+
safety: {
|
|
171
|
+
redactionApplied: redactedSummary.hitCount + redactedTitle.hitCount > 0 || Boolean(input.safety?.redactionApplied),
|
|
172
|
+
redactionHitCount: redactedSummary.hitCount + redactedTitle.hitCount + Number(input.safety?.redactionHitCount || 0),
|
|
173
|
+
rawContentUploaded: false
|
|
174
|
+
},
|
|
175
|
+
metadata: input.metadata && typeof input.metadata === "object" ? input.metadata : {}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export const EVENT_TYPE_DEFINITIONS = [
|
|
2
|
+
{ type: "session_start", label: "会话开始", category: "会话", description: "AI 工具会话被创建或恢复。", order: 10 },
|
|
3
|
+
{ type: "session_end", label: "会话结束", category: "会话", description: "AI 工具会话结束或被空闲关闭。", order: 20 },
|
|
4
|
+
{ type: "user_prompt", label: "用户提示", category: "对话", description: "员工向 AI 输入问题、需求或补充上下文。", order: 30 },
|
|
5
|
+
{ type: "assistant_response", label: "AI 回复", category: "对话", description: "AI 返回回答、方案、代码或解释。", order: 40 },
|
|
6
|
+
{ type: "planning", label: "任务规划", category: "过程", description: "进入计划、拆解、评估或方案设计阶段。", order: 50 },
|
|
7
|
+
{ type: "research", label: "资料调研", category: "过程", description: "检索、阅读、对比资料或代码上下文。", order: 60 },
|
|
8
|
+
{ type: "implementation", label: "实现修改", category: "过程", description: "生成、修改、重构代码或配置。", order: 70 },
|
|
9
|
+
{ type: "document", label: "文档材料", category: "过程", description: "生成或修改文档、报告、周报、方案材料。", order: 80 },
|
|
10
|
+
{ type: "tool_call", label: "工具调用", category: "工具", description: "AI 调用本地工具、读写文件、运行辅助能力。", order: 90 },
|
|
11
|
+
{ type: "tool_result", label: "工具结果", category: "工具", description: "工具调用完成并返回结果。", order: 100 },
|
|
12
|
+
{ type: "mcp_tool_call", label: "MCP 调用", category: "工具", description: "通过 MCP 调用外部能力或内部系统。", order: 110 },
|
|
13
|
+
{ type: "skill_use", label: "Skill 使用", category: "能力", description: "使用 Codex/Claude/OpenCode 的 Skill 工作流。", order: 120 },
|
|
14
|
+
{ type: "plugin_use", label: "插件使用", category: "能力", description: "使用 Figma、Browser、表格、邮件等插件能力。", order: 130 },
|
|
15
|
+
{ type: "command", label: "命令执行", category: "工具", description: "执行终端命令、脚本或构建任务。", order: 140 },
|
|
16
|
+
{ type: "mode_change", label: "模式切换", category: "过程", description: "AI 工具在聊天、规划、实现、审查等模式间切换。", order: 150 },
|
|
17
|
+
{ type: "permission", label: "权限决策", category: "治理", description: "员工确认、拒绝或授权执行高风险操作。", order: 160 },
|
|
18
|
+
{ type: "verification", label: "验证检查", category: "质量", description: "运行测试、构建、截图、人工验收或其它验证动作。", order: 170 },
|
|
19
|
+
{ type: "retry", label: "重试", category: "风险", description: "因失败、超时或结果不佳触发重试。", order: 180 },
|
|
20
|
+
{ type: "rework", label: "返工", category: "风险", description: "输出偏差、需求理解错误或质量问题导致返工。", order: 190 },
|
|
21
|
+
{ type: "error", label: "错误异常", category: "风险", description: "工具、脚本、模型或流程产生错误。", order: 200 }
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export const EVENT_TYPES = new Set(EVENT_TYPE_DEFINITIONS.map((item) => item.type));
|
|
25
|
+
|
|
26
|
+
export function eventTypeDefinition(type) {
|
|
27
|
+
return EVENT_TYPE_DEFINITIONS.find((item) => item.type === type) || EVENT_TYPE_DEFINITIONS.find((item) => item.type === "tool_call");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function eventTypeLabel(type) {
|
|
31
|
+
return eventTypeDefinition(type)?.label || "工具调用";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function eventTypeDistribution(events = [], { includeZero = false, limit = EVENT_TYPE_DEFINITIONS.length } = {}) {
|
|
35
|
+
const buckets = new Map(EVENT_TYPE_DEFINITIONS.map((item) => [
|
|
36
|
+
item.type,
|
|
37
|
+
{
|
|
38
|
+
type: item.type,
|
|
39
|
+
name: item.label,
|
|
40
|
+
category: item.category,
|
|
41
|
+
description: item.description,
|
|
42
|
+
order: item.order,
|
|
43
|
+
events: 0,
|
|
44
|
+
minutes: 0
|
|
45
|
+
}
|
|
46
|
+
]));
|
|
47
|
+
for (const event of events) {
|
|
48
|
+
const type = EVENT_TYPES.has(event.eventType) ? event.eventType : "tool_call";
|
|
49
|
+
const bucket = buckets.get(type);
|
|
50
|
+
bucket.events += 1;
|
|
51
|
+
bucket.minutes += Math.round(Number(event.metrics?.durationSeconds || 0) / 60);
|
|
52
|
+
}
|
|
53
|
+
return [...buckets.values()]
|
|
54
|
+
.filter((item) => includeZero || item.events > 0)
|
|
55
|
+
.sort((a, b) => {
|
|
56
|
+
if (b.events !== a.events) return b.events - a.events;
|
|
57
|
+
return a.order - b.order;
|
|
58
|
+
})
|
|
59
|
+
.slice(0, limit)
|
|
60
|
+
.map(({ order, ...item }) => item);
|
|
61
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const PROVIDER_HINTS = [
|
|
2
|
+
{ pattern: /gpt|o\d|codex/i, provider: "openai" },
|
|
3
|
+
{ pattern: /claude/i, provider: "anthropic" },
|
|
4
|
+
{ pattern: /gemini/i, provider: "google" },
|
|
5
|
+
{ pattern: /qwen|通义/i, provider: "alibaba" },
|
|
6
|
+
{ pattern: /deepseek/i, provider: "deepseek" },
|
|
7
|
+
{ pattern: /doubao|豆包/i, provider: "bytedance" }
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
export function normalizeModelInfo(input = {}) {
|
|
11
|
+
const source = input.model && typeof input.model === "object" ? input.model : input;
|
|
12
|
+
const name = stringValue(
|
|
13
|
+
source.name ||
|
|
14
|
+
source.model ||
|
|
15
|
+
source.modelName ||
|
|
16
|
+
source.model_name ||
|
|
17
|
+
source.id ||
|
|
18
|
+
input.modelName ||
|
|
19
|
+
input.model_name ||
|
|
20
|
+
input.metadata?.modelName ||
|
|
21
|
+
input.metadata?.model ||
|
|
22
|
+
""
|
|
23
|
+
);
|
|
24
|
+
const provider = stringValue(
|
|
25
|
+
source.provider ||
|
|
26
|
+
source.vendor ||
|
|
27
|
+
input.modelProvider ||
|
|
28
|
+
input.model_provider ||
|
|
29
|
+
input.metadata?.modelProvider ||
|
|
30
|
+
inferProvider(name)
|
|
31
|
+
);
|
|
32
|
+
const version = stringValue(
|
|
33
|
+
source.version ||
|
|
34
|
+
source.modelVersion ||
|
|
35
|
+
source.model_version ||
|
|
36
|
+
input.modelVersion ||
|
|
37
|
+
input.model_version ||
|
|
38
|
+
input.metadata?.modelVersion ||
|
|
39
|
+
inferVersion(name)
|
|
40
|
+
);
|
|
41
|
+
const family = stringValue(
|
|
42
|
+
source.family ||
|
|
43
|
+
input.modelFamily ||
|
|
44
|
+
input.model_family ||
|
|
45
|
+
input.metadata?.modelFamily ||
|
|
46
|
+
inferFamily(name)
|
|
47
|
+
);
|
|
48
|
+
return {
|
|
49
|
+
provider,
|
|
50
|
+
name,
|
|
51
|
+
version,
|
|
52
|
+
family,
|
|
53
|
+
label: [provider, name, version].filter(Boolean).join("/") || "unknown"
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function stringValue(value) {
|
|
58
|
+
return String(value || "").trim();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function inferProvider(name) {
|
|
62
|
+
for (const hint of PROVIDER_HINTS) {
|
|
63
|
+
if (hint.pattern.test(name)) return hint.provider;
|
|
64
|
+
}
|
|
65
|
+
return "";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function inferFamily(name) {
|
|
69
|
+
const source = String(name || "").toLowerCase();
|
|
70
|
+
if (!source) return "";
|
|
71
|
+
if (source.includes("codex")) return "codex";
|
|
72
|
+
if (source.includes("gpt")) return "gpt";
|
|
73
|
+
if (source.includes("claude")) return "claude";
|
|
74
|
+
if (source.includes("gemini")) return "gemini";
|
|
75
|
+
if (source.includes("qwen")) return "qwen";
|
|
76
|
+
if (source.includes("deepseek")) return "deepseek";
|
|
77
|
+
if (source.includes("doubao")) return "doubao";
|
|
78
|
+
return source.split(/[-_:./\s]/).filter(Boolean)[0] || "";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function inferVersion(name) {
|
|
82
|
+
const source = String(name || "");
|
|
83
|
+
const match = source.match(/(?:gpt-|claude-|gemini-|qwen-|deepseek-|doubao-|o)([0-9][A-Za-z0-9.-]*)/i);
|
|
84
|
+
return match?.[1] || "";
|
|
85
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
const COMMON_PINYIN = {
|
|
2
|
+
张: "zhang",
|
|
3
|
+
王: "wang",
|
|
4
|
+
李: "li",
|
|
5
|
+
赵: "zhao",
|
|
6
|
+
刘: "liu",
|
|
7
|
+
陈: "chen",
|
|
8
|
+
杨: "yang",
|
|
9
|
+
黄: "huang",
|
|
10
|
+
周: "zhou",
|
|
11
|
+
吴: "wu",
|
|
12
|
+
徐: "xu",
|
|
13
|
+
孙: "sun",
|
|
14
|
+
胡: "hu",
|
|
15
|
+
朱: "zhu",
|
|
16
|
+
高: "gao",
|
|
17
|
+
林: "lin",
|
|
18
|
+
何: "he",
|
|
19
|
+
郭: "guo",
|
|
20
|
+
马: "ma",
|
|
21
|
+
罗: "luo",
|
|
22
|
+
梁: "liang",
|
|
23
|
+
宋: "song",
|
|
24
|
+
郑: "zheng",
|
|
25
|
+
谢: "xie",
|
|
26
|
+
韩: "han",
|
|
27
|
+
唐: "tang",
|
|
28
|
+
冯: "feng",
|
|
29
|
+
于: "yu",
|
|
30
|
+
董: "dong",
|
|
31
|
+
萧: "xiao",
|
|
32
|
+
程: "cheng",
|
|
33
|
+
曹: "cao",
|
|
34
|
+
袁: "yuan",
|
|
35
|
+
邓: "deng",
|
|
36
|
+
许: "xu",
|
|
37
|
+
傅: "fu",
|
|
38
|
+
沈: "shen",
|
|
39
|
+
曾: "zeng",
|
|
40
|
+
彭: "peng",
|
|
41
|
+
吕: "lv",
|
|
42
|
+
苏: "su",
|
|
43
|
+
卢: "lu",
|
|
44
|
+
蒋: "jiang",
|
|
45
|
+
蔡: "cai",
|
|
46
|
+
贾: "jia",
|
|
47
|
+
丁: "ding",
|
|
48
|
+
魏: "wei",
|
|
49
|
+
薛: "xue",
|
|
50
|
+
叶: "ye",
|
|
51
|
+
阎: "yan",
|
|
52
|
+
余: "yu",
|
|
53
|
+
潘: "pan",
|
|
54
|
+
杜: "du",
|
|
55
|
+
戴: "dai",
|
|
56
|
+
夏: "xia",
|
|
57
|
+
钟: "zhong",
|
|
58
|
+
汪: "wang",
|
|
59
|
+
田: "tian",
|
|
60
|
+
任: "ren",
|
|
61
|
+
姜: "jiang",
|
|
62
|
+
范: "fan",
|
|
63
|
+
方: "fang",
|
|
64
|
+
石: "shi",
|
|
65
|
+
姚: "yao",
|
|
66
|
+
谭: "tan",
|
|
67
|
+
廖: "liao",
|
|
68
|
+
邹: "zou",
|
|
69
|
+
熊: "xiong",
|
|
70
|
+
金: "jin",
|
|
71
|
+
陆: "lu",
|
|
72
|
+
郝: "hao",
|
|
73
|
+
孔: "kong",
|
|
74
|
+
白: "bai",
|
|
75
|
+
崔: "cui",
|
|
76
|
+
康: "kang",
|
|
77
|
+
毛: "mao",
|
|
78
|
+
邱: "qiu",
|
|
79
|
+
秦: "qin",
|
|
80
|
+
江: "jiang",
|
|
81
|
+
史: "shi",
|
|
82
|
+
顾: "gu",
|
|
83
|
+
侯: "hou",
|
|
84
|
+
邵: "shao",
|
|
85
|
+
孟: "meng",
|
|
86
|
+
龙: "long",
|
|
87
|
+
万: "wan",
|
|
88
|
+
段: "duan",
|
|
89
|
+
雷: "lei",
|
|
90
|
+
钱: "qian",
|
|
91
|
+
汤: "tang",
|
|
92
|
+
尹: "yin",
|
|
93
|
+
易: "yi",
|
|
94
|
+
常: "chang",
|
|
95
|
+
武: "wu",
|
|
96
|
+
乔: "qiao",
|
|
97
|
+
贺: "he",
|
|
98
|
+
赖: "lai",
|
|
99
|
+
龚: "gong",
|
|
100
|
+
文: "wen",
|
|
101
|
+
三: "san",
|
|
102
|
+
四: "si",
|
|
103
|
+
五: "wu",
|
|
104
|
+
六: "liu",
|
|
105
|
+
七: "qi",
|
|
106
|
+
八: "ba",
|
|
107
|
+
九: "jiu",
|
|
108
|
+
十: "shi",
|
|
109
|
+
一: "yi",
|
|
110
|
+
二: "er",
|
|
111
|
+
小: "xiao",
|
|
112
|
+
明: "ming",
|
|
113
|
+
华: "hua",
|
|
114
|
+
强: "qiang",
|
|
115
|
+
磊: "lei",
|
|
116
|
+
军: "jun",
|
|
117
|
+
洋: "yang",
|
|
118
|
+
勇: "yong",
|
|
119
|
+
艳: "yan",
|
|
120
|
+
杰: "jie",
|
|
121
|
+
娟: "juan",
|
|
122
|
+
敏: "min",
|
|
123
|
+
静: "jing",
|
|
124
|
+
丽: "li",
|
|
125
|
+
超: "chao",
|
|
126
|
+
平: "ping",
|
|
127
|
+
刚: "gang",
|
|
128
|
+
芳: "fang",
|
|
129
|
+
霞: "xia",
|
|
130
|
+
秀: "xiu",
|
|
131
|
+
测: "ce",
|
|
132
|
+
试: "shi",
|
|
133
|
+
写: "xie",
|
|
134
|
+
入: "ru",
|
|
135
|
+
员: "yuan",
|
|
136
|
+
工: "gong",
|
|
137
|
+
前: "qian",
|
|
138
|
+
端: "duan",
|
|
139
|
+
后: "hou",
|
|
140
|
+
台: "tai",
|
|
141
|
+
平: "ping",
|
|
142
|
+
产: "chan",
|
|
143
|
+
品: "pin",
|
|
144
|
+
研: "yan",
|
|
145
|
+
发: "fa",
|
|
146
|
+
解: "jie",
|
|
147
|
+
决: "jue",
|
|
148
|
+
方: "fang",
|
|
149
|
+
案: "an",
|
|
150
|
+
售: "shou",
|
|
151
|
+
顾: "gu",
|
|
152
|
+
问: "wen"
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export function toPinyinSlug(value, fallback = "unknown") {
|
|
156
|
+
const input = String(value || "").trim();
|
|
157
|
+
if (!input) return fallback;
|
|
158
|
+
const output = [];
|
|
159
|
+
for (const char of input) {
|
|
160
|
+
if (/[a-zA-Z0-9]/.test(char)) {
|
|
161
|
+
output.push(char.toLowerCase());
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (/[\s._-]/.test(char)) {
|
|
165
|
+
if (output.at(-1) !== "-") output.push("-");
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (COMMON_PINYIN[char]) {
|
|
169
|
+
output.push(COMMON_PINYIN[char]);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const code = char.codePointAt(0).toString(16);
|
|
173
|
+
output.push(`u${code}`);
|
|
174
|
+
}
|
|
175
|
+
return output.join("").replace(/-+/g, "-").replace(/^-|-$/g, "") || fallback;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function employeeRegistrationName(employee = {}) {
|
|
179
|
+
return String(employee.pinyinName || employee.registrationName || "").trim()
|
|
180
|
+
|| toPinyinSlug(employee.name || employee.employeeName || "", String(employee.employeeId || employee.id || "unknown"));
|
|
181
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const TOOL_ALIASES = new Map([
|
|
2
|
+
["codex", "codex"],
|
|
3
|
+
["openai-codex", "codex"],
|
|
4
|
+
["claude", "claude-code"],
|
|
5
|
+
["claude-code", "claude-code"],
|
|
6
|
+
["calude-code", "claude-code"],
|
|
7
|
+
["calude code", "claude-code"],
|
|
8
|
+
["claude code", "claude-code"],
|
|
9
|
+
["opencode", "opencode"],
|
|
10
|
+
["open-code", "opencode"],
|
|
11
|
+
["open code", "opencode"]
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
export const TOOL_PROFILES = [
|
|
15
|
+
{
|
|
16
|
+
id: "codex",
|
|
17
|
+
label: "Codex",
|
|
18
|
+
vendor: "OpenAI",
|
|
19
|
+
configKind: "toml",
|
|
20
|
+
mcpServerName: "ai_worklens",
|
|
21
|
+
hookName: "ai-worklens-codex-hook",
|
|
22
|
+
configFileName: "codex-mcp-snippet.toml",
|
|
23
|
+
hookFileName: "codex-hook.sh",
|
|
24
|
+
installNote: "合并到 ~/.codex/config.toml,并配置 hook 脚本即可采集 Codex 事件。"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "claude-code",
|
|
28
|
+
label: "Claude Code",
|
|
29
|
+
vendor: "Anthropic",
|
|
30
|
+
configKind: "mcp-json",
|
|
31
|
+
mcpServerName: "ai-worklens",
|
|
32
|
+
hookName: "ai-worklens-claude-hook",
|
|
33
|
+
configFileName: "claude-code-mcp.json",
|
|
34
|
+
hookFileName: "claude-code-hook.sh",
|
|
35
|
+
installNote: "可放入项目 .mcp.json,或用 claude mcp add-json 导入 stdio MCP server。"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "opencode",
|
|
39
|
+
label: "OpenCode",
|
|
40
|
+
vendor: "OpenCode",
|
|
41
|
+
configKind: "opencode-json",
|
|
42
|
+
mcpServerName: "ai_worklens",
|
|
43
|
+
hookName: "ai-worklens-opencode-hook",
|
|
44
|
+
configFileName: "opencode-mcp.jsonc",
|
|
45
|
+
hookFileName: "opencode-hook.sh",
|
|
46
|
+
installNote: "合并到 opencode.json / opencode.jsonc 的 mcp 配置下。"
|
|
47
|
+
}
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
export function normalizeToolId(value = "codex") {
|
|
51
|
+
const key = String(value || "codex").trim().toLowerCase();
|
|
52
|
+
return TOOL_ALIASES.get(key) || "codex";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getToolProfile(value = "codex") {
|
|
56
|
+
const id = normalizeToolId(value);
|
|
57
|
+
return TOOL_PROFILES.find((profile) => profile.id === id) || TOOL_PROFILES[0];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function listToolProfiles() {
|
|
61
|
+
return TOOL_PROFILES.map((profile) => ({ ...profile }));
|
|
62
|
+
}
|
package/src/team-rollout.mjs
CHANGED
|
@@ -3,10 +3,10 @@ import fs from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { normalizeToolId } from "
|
|
6
|
+
import { normalizeToolId } from "./protocol/tool-profiles.mjs";
|
|
7
7
|
|
|
8
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
const
|
|
9
|
+
const agentSrcDir = __dirname;
|
|
10
10
|
|
|
11
11
|
function parseArgs(argv) {
|
|
12
12
|
const result = {};
|
|
@@ -90,7 +90,7 @@ function buildRunnerScript() {
|
|
|
90
90
|
"import fs from \"node:fs\";",
|
|
91
91
|
"import os from \"node:os\";",
|
|
92
92
|
"import path from \"node:path\";",
|
|
93
|
-
`import { installClient } from ${JSON.stringify(path.join(
|
|
93
|
+
`import { installClient } from ${JSON.stringify(path.join(agentSrcDir, "install.mjs"))};`,
|
|
94
94
|
"",
|
|
95
95
|
"const packageDir = path.dirname(new URL(import.meta.url).pathname);",
|
|
96
96
|
"const manifest = JSON.parse(fs.readFileSync(path.join(packageDir, \"rollout-manifest.json\"), \"utf8\"));",
|
package/src/uploader.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { EventQueue } from "./queue.mjs";
|
|
3
3
|
import { writeClientConfig } from "./config.mjs";
|
|
4
|
-
import { normalizeCollectionSettings } from "
|
|
5
|
-
import { normalizeClientUpdatePolicy, updateNeeded } from "
|
|
4
|
+
import { normalizeCollectionSettings } from "./protocol/collection-settings.mjs";
|
|
5
|
+
import { normalizeClientUpdatePolicy, updateNeeded } from "./protocol/client-update-policy.mjs";
|
|
6
6
|
import { installClient } from "./install.mjs";
|
|
7
7
|
|
|
8
8
|
async function requestJson(url, config, options = {}) {
|