ai-worklens-agent 0.1.4 → 0.1.5
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 +2 -2
- package/package.json +2 -7
- package/src/config.mjs +5 -3
- package/src/install.mjs +1 -1
- package/src/protocol/client-update-policy.mjs +1 -1
- package/src/queue.mjs +158 -31
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ npm run mcp
|
|
|
36
36
|
如果管理员已经把员工端发布到 npm 或企业私有 npm 源,可以使用 `npx` 首次安装:
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
|
-
NPM_CONFIG_UPDATE_NOTIFIER=false npx -y --loglevel=error -p ai-worklens-agent@0.1.
|
|
39
|
+
NPM_CONFIG_UPDATE_NOTIFIER=false npx -y --loglevel=error -p ai-worklens-agent@0.1.5 worklens-agent-install \
|
|
40
40
|
--server-url http://192.168.1.241:8797 \
|
|
41
41
|
--tool codex \
|
|
42
42
|
--employee-pinyin zhangsan
|
|
@@ -60,7 +60,7 @@ NPM_TOKEN=<npm_token> npm run client:npm:publish -- \
|
|
|
60
60
|
如果管理员在官网发布了直链安装包,可以下载安装包后执行包内安装脚本:
|
|
61
61
|
|
|
62
62
|
```bash
|
|
63
|
-
curl -fL http://192.168.1.241:8797/site/downloads/ai-worklens-codex-0.1.
|
|
63
|
+
curl -fL http://192.168.1.241:8797/site/downloads/ai-worklens-codex-0.1.5.sh \
|
|
64
64
|
-o ai-worklens-install.sh
|
|
65
65
|
chmod +x ai-worklens-install.sh
|
|
66
66
|
./ai-worklens-install.sh zhangsan
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-worklens-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Employee-side collector agent for AI WorkLens.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,10 +18,5 @@
|
|
|
18
18
|
"claude-code",
|
|
19
19
|
"opencode"
|
|
20
20
|
],
|
|
21
|
-
"license": "UNLICENSED"
|
|
22
|
-
"private": false,
|
|
23
|
-
"publishConfig": {
|
|
24
|
-
"access": "public",
|
|
25
|
-
"registry": "https://registry.npmjs.org"
|
|
26
|
-
}
|
|
21
|
+
"license": "UNLICENSED"
|
|
27
22
|
}
|
package/src/config.mjs
CHANGED
|
@@ -137,10 +137,12 @@ export function loadClientConfig(options = {}) {
|
|
|
137
137
|
family: options.modelFamily || envValue(env, "WORKLENS_MODEL_FAMILY") || fileConfig.model?.family
|
|
138
138
|
});
|
|
139
139
|
const inferredModel = inferToolModel({ tool, homeDir, env });
|
|
140
|
+
const employeePinyin = options.employeePinyin || options.pinyinName || envValue(env, "WORKLENS_EMPLOYEE_PINYIN") || fileConfig.employee?.pinyinName || "";
|
|
141
|
+
const employeeId = options.employeeId || envValue(env, "WORKLENS_EMPLOYEE_ID") || fileConfig.employee?.id || employeePinyin || os.userInfo().username;
|
|
140
142
|
const employee = {
|
|
141
|
-
id:
|
|
142
|
-
name: options.employeeName || envValue(env, "WORKLENS_EMPLOYEE_NAME") || fileConfig.employee?.name || os.userInfo().username,
|
|
143
|
-
pinyinName:
|
|
143
|
+
id: employeeId,
|
|
144
|
+
name: options.employeeName || envValue(env, "WORKLENS_EMPLOYEE_NAME") || fileConfig.employee?.name || employeeId || os.userInfo().username,
|
|
145
|
+
pinyinName: employeePinyin,
|
|
144
146
|
department: options.department || envValue(env, "WORKLENS_DEPARTMENT") || fileConfig.employee?.department || "",
|
|
145
147
|
role: options.role || envValue(env, "WORKLENS_ROLE") || fileConfig.employee?.role || ""
|
|
146
148
|
};
|
package/src/install.mjs
CHANGED
|
@@ -152,7 +152,7 @@ export function installClient(options = {}) {
|
|
|
152
152
|
family: options.modelFamily || ""
|
|
153
153
|
},
|
|
154
154
|
employee: {
|
|
155
|
-
id: options.employeeId || "",
|
|
155
|
+
id: options.employeeId || options.employeePinyin || options.pinyinName || "",
|
|
156
156
|
name: options.employeeName || "",
|
|
157
157
|
pinyinName: options.employeePinyin || options.pinyinName || "",
|
|
158
158
|
department: options.department || "",
|
package/src/queue.mjs
CHANGED
|
@@ -7,11 +7,66 @@ const DEFAULT_RETRY = {
|
|
|
7
7
|
baseDelayMs: 30 * 1000
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
+
const LOCK_STALE_MS = 15 * 1000;
|
|
11
|
+
const LOCK_TIMEOUT_MS = 5 * 1000;
|
|
12
|
+
|
|
13
|
+
function sleep(ms) {
|
|
14
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseFirstJsonArray(raw) {
|
|
18
|
+
let started = false;
|
|
19
|
+
let depth = 0;
|
|
20
|
+
let inString = false;
|
|
21
|
+
let escaped = false;
|
|
22
|
+
|
|
23
|
+
for (let index = 0; index < raw.length; index += 1) {
|
|
24
|
+
const char = raw[index];
|
|
25
|
+
if (!started) {
|
|
26
|
+
if (/\s/.test(char)) continue;
|
|
27
|
+
if (char !== "[") return null;
|
|
28
|
+
started = true;
|
|
29
|
+
depth = 1;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (inString) {
|
|
34
|
+
if (escaped) {
|
|
35
|
+
escaped = false;
|
|
36
|
+
} else if (char === "\\") {
|
|
37
|
+
escaped = true;
|
|
38
|
+
} else if (char === "\"") {
|
|
39
|
+
inString = false;
|
|
40
|
+
}
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (char === "\"") {
|
|
45
|
+
inString = true;
|
|
46
|
+
} else if (char === "[") {
|
|
47
|
+
depth += 1;
|
|
48
|
+
} else if (char === "]") {
|
|
49
|
+
depth -= 1;
|
|
50
|
+
if (depth === 0) {
|
|
51
|
+
const parsed = JSON.parse(raw.slice(0, index + 1));
|
|
52
|
+
return Array.isArray(parsed) ? parsed : null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
10
59
|
async function readJsonArray(filePath) {
|
|
11
60
|
try {
|
|
12
61
|
const raw = await fs.readFile(filePath, "utf8");
|
|
13
|
-
|
|
14
|
-
|
|
62
|
+
try {
|
|
63
|
+
const parsed = JSON.parse(raw);
|
|
64
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
65
|
+
} catch (error) {
|
|
66
|
+
const recovered = parseFirstJsonArray(raw);
|
|
67
|
+
if (recovered) return recovered;
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
15
70
|
} catch (error) {
|
|
16
71
|
if (error.code === "ENOENT") return [];
|
|
17
72
|
throw error;
|
|
@@ -44,30 +99,96 @@ function retryDelay(attempts, options = {}) {
|
|
|
44
99
|
return Math.min(maxDelayMs, baseDelayMs * (2 ** exponent));
|
|
45
100
|
}
|
|
46
101
|
|
|
102
|
+
async function writeJsonArrayAtomic(filePath, items) {
|
|
103
|
+
const directory = path.dirname(filePath);
|
|
104
|
+
await fs.mkdir(directory, { recursive: true });
|
|
105
|
+
const tempPath = path.join(directory, `.${path.basename(filePath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
106
|
+
try {
|
|
107
|
+
await fs.writeFile(tempPath, `${JSON.stringify(items.map(normalizeItem), null, 2)}\n`, { mode: 0o600 });
|
|
108
|
+
await fs.rename(tempPath, filePath);
|
|
109
|
+
await fs.chmod(filePath, 0o600).catch(() => {});
|
|
110
|
+
} catch (error) {
|
|
111
|
+
await fs.unlink(tempPath).catch(() => {});
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function acquireQueueLock(filePath, options = {}) {
|
|
117
|
+
const lockPath = `${filePath}.lock`;
|
|
118
|
+
const staleMs = Number(options.staleMs || LOCK_STALE_MS);
|
|
119
|
+
const timeoutMs = Number(options.timeoutMs || LOCK_TIMEOUT_MS);
|
|
120
|
+
const startedAt = Date.now();
|
|
121
|
+
|
|
122
|
+
while (true) {
|
|
123
|
+
try {
|
|
124
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
125
|
+
const handle = await fs.open(lockPath, "wx", 0o600);
|
|
126
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }));
|
|
127
|
+
return async () => {
|
|
128
|
+
await handle.close().catch(() => {});
|
|
129
|
+
await fs.unlink(lockPath).catch(() => {});
|
|
130
|
+
};
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (error.code !== "EEXIST") throw error;
|
|
133
|
+
const stats = await fs.stat(lockPath).catch(() => null);
|
|
134
|
+
if (stats && Date.now() - stats.mtimeMs > staleMs) {
|
|
135
|
+
await fs.unlink(lockPath).catch(() => {});
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (Date.now() - startedAt > timeoutMs) {
|
|
139
|
+
throw new Error(`queue lock timeout: ${lockPath}`);
|
|
140
|
+
}
|
|
141
|
+
await sleep(25 + Math.floor(Math.random() * 50));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
47
146
|
export class EventQueue {
|
|
48
147
|
constructor(filePath) {
|
|
49
148
|
this.filePath = filePath;
|
|
50
149
|
}
|
|
51
150
|
|
|
52
|
-
async
|
|
151
|
+
async withLock(fn) {
|
|
152
|
+
const release = await acquireQueueLock(this.filePath);
|
|
153
|
+
try {
|
|
154
|
+
return await fn();
|
|
155
|
+
} finally {
|
|
156
|
+
await release();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async listUnlocked() {
|
|
53
161
|
const items = await readJsonArray(this.filePath);
|
|
54
162
|
return items.map(normalizeItem);
|
|
55
163
|
}
|
|
56
164
|
|
|
165
|
+
async saveUnlocked(items) {
|
|
166
|
+
await writeJsonArrayAtomic(this.filePath, items);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async list() {
|
|
170
|
+
return this.listUnlocked();
|
|
171
|
+
}
|
|
172
|
+
|
|
57
173
|
async save(items) {
|
|
58
|
-
await
|
|
59
|
-
|
|
174
|
+
await this.withLock(async () => {
|
|
175
|
+
await this.saveUnlocked(items);
|
|
176
|
+
});
|
|
60
177
|
}
|
|
61
178
|
|
|
62
179
|
async enqueue(event) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
180
|
+
return this.withLock(async () => {
|
|
181
|
+
const items = await this.listUnlocked();
|
|
182
|
+
items.push(normalizeItem({ queuedAt: new Date().toISOString(), event }));
|
|
183
|
+
await this.saveUnlocked(items);
|
|
184
|
+
return items.length;
|
|
185
|
+
});
|
|
67
186
|
}
|
|
68
187
|
|
|
69
188
|
async replace(items) {
|
|
70
|
-
await this.
|
|
189
|
+
await this.withLock(async () => {
|
|
190
|
+
await this.saveUnlocked(items);
|
|
191
|
+
});
|
|
71
192
|
}
|
|
72
193
|
|
|
73
194
|
async due({ now = new Date(), limit = Infinity, force = false } = {}) {
|
|
@@ -79,30 +200,34 @@ export class EventQueue {
|
|
|
79
200
|
}
|
|
80
201
|
|
|
81
202
|
async markFailed(failedIds, error, { now = new Date(), retry = DEFAULT_RETRY } = {}) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
203
|
+
return this.withLock(async () => {
|
|
204
|
+
const idSet = new Set(failedIds);
|
|
205
|
+
const nowIso = now instanceof Date ? now.toISOString() : new Date(now).toISOString();
|
|
206
|
+
const items = await this.listUnlocked();
|
|
207
|
+
const marked = items.map((item) => {
|
|
208
|
+
if (!idSet.has(item.id)) return item;
|
|
209
|
+
const attempts = Number(item.attempts || 0) + 1;
|
|
210
|
+
return {
|
|
211
|
+
...item,
|
|
212
|
+
attempts,
|
|
213
|
+
lastAttemptAt: nowIso,
|
|
214
|
+
lastError: String(error?.message || error || "upload_failed"),
|
|
215
|
+
nextAttemptAt: new Date(Date.parse(nowIso) + retryDelay(attempts, retry)).toISOString()
|
|
216
|
+
};
|
|
217
|
+
});
|
|
218
|
+
await this.saveUnlocked(marked);
|
|
219
|
+
return marked;
|
|
95
220
|
});
|
|
96
|
-
await this.save(marked);
|
|
97
|
-
return marked;
|
|
98
221
|
}
|
|
99
222
|
|
|
100
223
|
async remove(removeIds) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
224
|
+
return this.withLock(async () => {
|
|
225
|
+
const idSet = new Set(removeIds);
|
|
226
|
+
const items = await this.listUnlocked();
|
|
227
|
+
const remaining = items.filter((item) => !idSet.has(item.id));
|
|
228
|
+
await this.saveUnlocked(remaining);
|
|
229
|
+
return remaining;
|
|
230
|
+
});
|
|
106
231
|
}
|
|
107
232
|
|
|
108
233
|
async stats({ now = new Date() } = {}) {
|
|
@@ -123,6 +248,8 @@ export class EventQueue {
|
|
|
123
248
|
}
|
|
124
249
|
|
|
125
250
|
async clear() {
|
|
126
|
-
await this.
|
|
251
|
+
await this.withLock(async () => {
|
|
252
|
+
await this.saveUnlocked([]);
|
|
253
|
+
});
|
|
127
254
|
}
|
|
128
255
|
}
|