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 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.4 worklens-agent-install \
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.4.sh \
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.4",
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: options.employeeId || envValue(env, "WORKLENS_EMPLOYEE_ID") || fileConfig.employee?.id || os.userInfo().username,
142
- name: options.employeeName || envValue(env, "WORKLENS_EMPLOYEE_NAME") || fileConfig.employee?.name || os.userInfo().username,
143
- pinyinName: options.employeePinyin || options.pinyinName || envValue(env, "WORKLENS_EMPLOYEE_PINYIN") || fileConfig.employee?.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 || "",
@@ -1,4 +1,4 @@
1
- export const CLIENT_AGENT_VERSION = "0.1.4";
1
+ export const CLIENT_AGENT_VERSION = "0.1.5";
2
2
 
3
3
  export const DEFAULT_CLIENT_UPDATE_POLICY = {
4
4
  enabled: true,
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
- const parsed = JSON.parse(raw);
14
- return Array.isArray(parsed) ? parsed : [];
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 list() {
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 fs.mkdir(path.dirname(this.filePath), { recursive: true });
59
- await fs.writeFile(this.filePath, `${JSON.stringify(items.map(normalizeItem), null, 2)}\n`, { mode: 0o600 });
174
+ await this.withLock(async () => {
175
+ await this.saveUnlocked(items);
176
+ });
60
177
  }
61
178
 
62
179
  async enqueue(event) {
63
- const items = await this.list();
64
- items.push(normalizeItem({ queuedAt: new Date().toISOString(), event }));
65
- await this.save(items);
66
- return items.length;
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.save(items);
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
- const idSet = new Set(failedIds);
83
- const nowIso = now instanceof Date ? now.toISOString() : new Date(now).toISOString();
84
- const items = await this.list();
85
- const marked = items.map((item) => {
86
- if (!idSet.has(item.id)) return item;
87
- const attempts = Number(item.attempts || 0) + 1;
88
- return {
89
- ...item,
90
- attempts,
91
- lastAttemptAt: nowIso,
92
- lastError: String(error?.message || error || "upload_failed"),
93
- nextAttemptAt: new Date(Date.parse(nowIso) + retryDelay(attempts, retry)).toISOString()
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
- const idSet = new Set(removeIds);
102
- const items = await this.list();
103
- const remaining = items.filter((item) => !idSet.has(item.id));
104
- await this.save(remaining);
105
- return remaining;
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.save([]);
251
+ await this.withLock(async () => {
252
+ await this.saveUnlocked([]);
253
+ });
127
254
  }
128
255
  }