ai-dev-requirements 0.4.0 → 0.5.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/dist/index.cjs +672 -133
- package/dist/index.mjs +671 -132
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -185,7 +185,7 @@ function loadConfig(startDir) {
|
|
|
185
185
|
}
|
|
186
186
|
//#endregion
|
|
187
187
|
//#region package.json
|
|
188
|
-
var version = "0.
|
|
188
|
+
var version = "0.5.1";
|
|
189
189
|
//#endregion
|
|
190
190
|
//#region ../../src/utils/ones-issue-kind.ts
|
|
191
191
|
/**
|
|
@@ -265,18 +265,34 @@ var BaseAdapter = class {
|
|
|
265
265
|
async updateWikiPage(_params) {
|
|
266
266
|
throw new Error(`${this.sourceType}: Wiki update endpoint is not verified`);
|
|
267
267
|
}
|
|
268
|
+
/** Production Wiki deletes stay disabled until the exact provider endpoint is verified. */
|
|
269
|
+
async deleteWikiPage(_params) {
|
|
270
|
+
throw new Error(`${this.sourceType}: Wiki delete endpoint is not verified`);
|
|
271
|
+
}
|
|
268
272
|
};
|
|
269
273
|
//#endregion
|
|
270
274
|
//#region ../../src/adapters/ones/api-client.ts
|
|
271
275
|
function base64Url(buffer) {
|
|
272
276
|
return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
273
277
|
}
|
|
274
|
-
function getSetCookies(response) {
|
|
278
|
+
function getSetCookies$1(response) {
|
|
279
|
+
if (!response.headers) return [];
|
|
275
280
|
const headers = response.headers;
|
|
276
281
|
if (headers.getSetCookie) return headers.getSetCookie();
|
|
277
282
|
const raw = response.headers.get("set-cookie");
|
|
278
283
|
return raw ? [raw] : [];
|
|
279
284
|
}
|
|
285
|
+
function mergeResponseCookies(cookieJar, response) {
|
|
286
|
+
for (const cookie of getSetCookies$1(response)) {
|
|
287
|
+
const pair = cookie.split(";")[0];
|
|
288
|
+
const separator = pair.indexOf("=");
|
|
289
|
+
if (separator <= 0) continue;
|
|
290
|
+
cookieJar.set(pair.slice(0, separator), pair.slice(separator + 1));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function serializeCookies(cookieJar) {
|
|
294
|
+
return [...cookieJar].map(([name, value]) => `${name}=${value}`).join("; ");
|
|
295
|
+
}
|
|
280
296
|
function parseRedirectValue(location, names) {
|
|
281
297
|
try {
|
|
282
298
|
const parsed = new URL(location);
|
|
@@ -304,6 +320,7 @@ var OnesApiClient = class {
|
|
|
304
320
|
const email = this.resolvedAuth.email;
|
|
305
321
|
const password = this.resolvedAuth.password;
|
|
306
322
|
if (!email || !password) throw new Error("ONES auth requires email and password (ones-pkce auth type)");
|
|
323
|
+
const cookieJar = /* @__PURE__ */ new Map();
|
|
307
324
|
const certRes = await fetch(`${baseUrl}/identity/api/encryption_cert`, {
|
|
308
325
|
method: "POST",
|
|
309
326
|
headers: { "Content-Type": "application/json" },
|
|
@@ -324,11 +341,15 @@ var OnesApiClient = class {
|
|
|
324
341
|
})
|
|
325
342
|
});
|
|
326
343
|
if (!loginRes.ok) throw new Error(`ONES: Login failed with status ${loginRes.status}`);
|
|
327
|
-
|
|
344
|
+
mergeResponseCookies(cookieJar, loginRes);
|
|
328
345
|
const loginData = await loginRes.json();
|
|
329
346
|
const configuredOrgUuid = this.config.options?.orgUuid;
|
|
330
347
|
const orgUser = configuredOrgUuid ? loginData.org_users.find((user) => user.org_uuid === configuredOrgUuid) ?? loginData.org_users[0] : loginData.org_users[0];
|
|
331
348
|
if (!orgUser) throw new Error("ONES: No organizations found for this user");
|
|
349
|
+
cookieJar.set("ones-region-uuid", orgUser.region_uuid);
|
|
350
|
+
cookieJar.set("ones-org-uuid", orgUser.org_uuid);
|
|
351
|
+
const timezone = cookieJar.get("ones-tz");
|
|
352
|
+
if (timezone) cookieJar.set("timezone", timezone);
|
|
332
353
|
const codeVerifier = base64Url(crypto.randomBytes(32));
|
|
333
354
|
const codeChallenge = base64Url(crypto.createHash("sha256").update(codeVerifier).digest());
|
|
334
355
|
const authorizeParams = new URLSearchParams({
|
|
@@ -340,15 +361,17 @@ var OnesApiClient = class {
|
|
|
340
361
|
redirect_uri: `${baseUrl}/auth/authorize/callback`,
|
|
341
362
|
state: `org_uuid=${orgUser.org_uuid}`
|
|
342
363
|
});
|
|
343
|
-
const
|
|
364
|
+
const authorizeRes = await fetch(`${baseUrl}/identity/authorize`, {
|
|
344
365
|
method: "POST",
|
|
345
366
|
headers: {
|
|
346
367
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
347
|
-
"Cookie":
|
|
368
|
+
"Cookie": serializeCookies(cookieJar)
|
|
348
369
|
},
|
|
349
370
|
body: authorizeParams.toString(),
|
|
350
371
|
redirect: "manual"
|
|
351
|
-
})
|
|
372
|
+
});
|
|
373
|
+
mergeResponseCookies(cookieJar, authorizeRes);
|
|
374
|
+
const authorizeLocation = authorizeRes.headers.get("location");
|
|
352
375
|
if (!authorizeLocation) throw new Error("ONES: Authorize response missing location header");
|
|
353
376
|
let code = parseRedirectValue(authorizeLocation, ["code"]);
|
|
354
377
|
if (!code) {
|
|
@@ -358,7 +381,7 @@ var OnesApiClient = class {
|
|
|
358
381
|
method: "POST",
|
|
359
382
|
headers: {
|
|
360
383
|
"Content-Type": "application/json;charset=UTF-8",
|
|
361
|
-
"Cookie":
|
|
384
|
+
"Cookie": serializeCookies(cookieJar)
|
|
362
385
|
},
|
|
363
386
|
body: JSON.stringify({
|
|
364
387
|
auth_request_id: authRequestId,
|
|
@@ -367,12 +390,15 @@ var OnesApiClient = class {
|
|
|
367
390
|
org_user_uuid: orgUser.org_user.org_user_uuid
|
|
368
391
|
})
|
|
369
392
|
});
|
|
393
|
+
mergeResponseCookies(cookieJar, finalizeRes);
|
|
370
394
|
if (!finalizeRes.ok) throw new Error(`ONES: Finalize failed with status ${finalizeRes.status}`);
|
|
371
|
-
const
|
|
395
|
+
const callbackRes = await fetch(`${baseUrl}/identity/authorize/callback?id=${authRequestId}&lang=zh`, {
|
|
372
396
|
method: "GET",
|
|
373
|
-
headers: { Cookie:
|
|
397
|
+
headers: { Cookie: serializeCookies(cookieJar) },
|
|
374
398
|
redirect: "manual"
|
|
375
|
-
})
|
|
399
|
+
});
|
|
400
|
+
mergeResponseCookies(cookieJar, callbackRes);
|
|
401
|
+
const callbackLocation = callbackRes.headers.get("location");
|
|
376
402
|
if (!callbackLocation) throw new Error("ONES: Callback response missing location header");
|
|
377
403
|
code = parseRedirectValue(callbackLocation, ["code"]);
|
|
378
404
|
}
|
|
@@ -381,7 +407,7 @@ var OnesApiClient = class {
|
|
|
381
407
|
method: "POST",
|
|
382
408
|
headers: {
|
|
383
409
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
384
|
-
"Cookie":
|
|
410
|
+
"Cookie": serializeCookies(cookieJar)
|
|
385
411
|
},
|
|
386
412
|
body: new URLSearchParams({
|
|
387
413
|
grant_type: "authorization_code",
|
|
@@ -391,16 +417,20 @@ var OnesApiClient = class {
|
|
|
391
417
|
redirect_uri: `${baseUrl}/auth/authorize/callback`
|
|
392
418
|
}).toString()
|
|
393
419
|
});
|
|
420
|
+
mergeResponseCookies(cookieJar, tokenRes);
|
|
394
421
|
if (!tokenRes.ok) throw new Error(`ONES: Token exchange failed with status ${tokenRes.status}`);
|
|
395
422
|
const token = await tokenRes.json();
|
|
423
|
+
cookieJar.set("ones-lt", token.access_token);
|
|
396
424
|
const teamsRes = await fetch(`${baseUrl}/project/api/project/organization/${orgUser.org_uuid}/stamps/data?t=org_my_team`, {
|
|
397
425
|
method: "POST",
|
|
398
426
|
headers: {
|
|
399
427
|
"Authorization": `Bearer ${token.access_token}`,
|
|
400
|
-
"Content-Type": "application/json;charset=UTF-8"
|
|
428
|
+
"Content-Type": "application/json;charset=UTF-8",
|
|
429
|
+
"Cookie": serializeCookies(cookieJar)
|
|
401
430
|
},
|
|
402
431
|
body: JSON.stringify({ org_my_team: 0 })
|
|
403
432
|
});
|
|
433
|
+
mergeResponseCookies(cookieJar, teamsRes);
|
|
404
434
|
if (!teamsRes.ok) throw new Error(`ONES: Failed to fetch teams: ${teamsRes.status}`);
|
|
405
435
|
const teams = (await teamsRes.json()).org_my_team?.teams ?? [];
|
|
406
436
|
const configuredTeamUuid = this.config.options?.teamUuid;
|
|
@@ -412,6 +442,9 @@ var OnesApiClient = class {
|
|
|
412
442
|
orgUuid: orgUser.org_uuid,
|
|
413
443
|
userUuid: orgUser.org_user.org_user_uuid,
|
|
414
444
|
userName: orgUser.org_user.name,
|
|
445
|
+
cookieHeader: serializeCookies(cookieJar),
|
|
446
|
+
legacyAuthToken: loginData.sid,
|
|
447
|
+
legacyUserId: loginData.auth_user_uuid,
|
|
415
448
|
expiresAt: Date.now() + (token.expires_in - 60) * 1e3
|
|
416
449
|
};
|
|
417
450
|
return this.session;
|
|
@@ -644,10 +677,11 @@ function mapOnesTypeFromTask(task) {
|
|
|
644
677
|
return mapOnesType(task.subIssueType?.name ?? task.issueType?.name ?? "");
|
|
645
678
|
}
|
|
646
679
|
function toRequirement(task, description = "", attachments = []) {
|
|
680
|
+
const displayId = taskDisplayId({}, task, task.project?.identifier?.toUpperCase() ?? null);
|
|
647
681
|
return {
|
|
648
682
|
id: task.uuid,
|
|
649
683
|
source: "ones",
|
|
650
|
-
title:
|
|
684
|
+
title: `${displayId} ${task.name}`,
|
|
651
685
|
description,
|
|
652
686
|
status: mapOnesStatus(task.status?.category ?? "to_do"),
|
|
653
687
|
priority: mapOnesPriority(task.priority?.value ?? "normal"),
|
|
@@ -788,7 +822,8 @@ function removeControlCharacters(value) {
|
|
|
788
822
|
return output;
|
|
789
823
|
}
|
|
790
824
|
function sanitizeExternalText(value) {
|
|
791
|
-
|
|
825
|
+
const withoutActiveContent = value.slice(0, MAX_EXTERNAL_TEXT_CHARS).replace(/<(?:script|style|iframe|object|embed)\b[^>]*>[\s\S]*?<\/(?:script|style|iframe|object|embed)>/gi, "").replace(/<img\b[^>]*>/gi, "[Image omitted]").replace(/<br\s*\/?>/gi, "\n").replace(/<\/p\s*>/gi, "\n").replace(/<\/(?:td|th)\s*>/gi, " | ").replace(/<\/tr\s*>/gi, "\n").replace(/<[^>]+>/g, "");
|
|
826
|
+
return removeControlCharacters(removeUrlCredentials(decodeHTML(withoutActiveContent))).replace(/[ \t]+\n/g, "\n").replace(/\n[ \t]+/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
792
827
|
}
|
|
793
828
|
function sanitizeExternalInline(value) {
|
|
794
829
|
return sanitizeExternalText(value).replace(/\s+/g, " ").slice(0, MAX_EXTERNAL_INLINE_CHARS);
|
|
@@ -1166,17 +1201,43 @@ function renderWikiContent(content, context = { imageSources: [] }) {
|
|
|
1166
1201
|
return asWikiBlocks(document.blocks).map((block) => renderWikiBlock(block, document, context)).filter(Boolean).join("\n\n").replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
1167
1202
|
}
|
|
1168
1203
|
function newWikiBlockId() {
|
|
1169
|
-
|
|
1204
|
+
for (;;) {
|
|
1205
|
+
const id = randomBytes(9).toString("base64url").replace(/[^a-z0-9]/gi, "").slice(0, 9);
|
|
1206
|
+
if (/^[a-z][a-z0-9]{8}$/i.test(id)) return id;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
function markdownTextRuns(text) {
|
|
1210
|
+
if (!text) return [];
|
|
1211
|
+
const runs = [];
|
|
1212
|
+
const inlinePattern = /\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)|`([^`]+)`/g;
|
|
1213
|
+
let cursor = 0;
|
|
1214
|
+
for (const match of text.matchAll(inlinePattern)) {
|
|
1215
|
+
const index = match.index ?? 0;
|
|
1216
|
+
if (index > cursor) runs.push({ insert: text.slice(cursor, index) });
|
|
1217
|
+
if (match[1] && match[2]) runs.push({
|
|
1218
|
+
insert: match[1],
|
|
1219
|
+
attributes: { link: match[2] }
|
|
1220
|
+
});
|
|
1221
|
+
else if (match[3]) runs.push({
|
|
1222
|
+
insert: match[3],
|
|
1223
|
+
attributes: { "style-code": true }
|
|
1224
|
+
});
|
|
1225
|
+
cursor = index + match[0].length;
|
|
1226
|
+
}
|
|
1227
|
+
if (cursor < text.length) runs.push({ insert: text.slice(cursor) });
|
|
1228
|
+
return runs.length ? runs : [{ insert: text }];
|
|
1170
1229
|
}
|
|
1171
1230
|
function wikiTextBlock(text, options = {}) {
|
|
1172
1231
|
return {
|
|
1173
1232
|
id: newWikiBlockId(),
|
|
1174
1233
|
type: options.list ? "list" : "text",
|
|
1175
|
-
text: text
|
|
1234
|
+
text: markdownTextRuns(text),
|
|
1176
1235
|
...options.heading ? { heading: options.heading } : {},
|
|
1177
1236
|
...options.list ? {
|
|
1178
1237
|
ordered: options.ordered ?? false,
|
|
1179
|
-
level: 1
|
|
1238
|
+
level: options.level ?? 1,
|
|
1239
|
+
...options.start === void 0 ? {} : { start: options.start },
|
|
1240
|
+
...options.groupId ? { groupId: options.groupId } : {}
|
|
1180
1241
|
} : {}
|
|
1181
1242
|
};
|
|
1182
1243
|
}
|
|
@@ -1187,6 +1248,9 @@ function isMarkdownSeparatorRow(line) {
|
|
|
1187
1248
|
const cells = parseMarkdownRow(line);
|
|
1188
1249
|
return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell));
|
|
1189
1250
|
}
|
|
1251
|
+
function estimateWikiTableCellWidth(value) {
|
|
1252
|
+
return [...value].reduce((width, character) => width + (character.codePointAt(0) > 127 ? 14 : 7), 24);
|
|
1253
|
+
}
|
|
1190
1254
|
function markdownToWikiDocument(markdown) {
|
|
1191
1255
|
const document = {
|
|
1192
1256
|
blocks: [],
|
|
@@ -1195,9 +1259,15 @@ function markdownToWikiDocument(markdown) {
|
|
|
1195
1259
|
};
|
|
1196
1260
|
const blocks = document.blocks;
|
|
1197
1261
|
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
|
|
1262
|
+
let activeList = null;
|
|
1198
1263
|
for (let index = 0; index < lines.length; index += 1) {
|
|
1199
1264
|
const line = lines[index];
|
|
1265
|
+
if (!line.trim()) {
|
|
1266
|
+
activeList = null;
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1200
1269
|
if (line.includes("|") && lines[index + 1] && isMarkdownSeparatorRow(lines[index + 1])) {
|
|
1270
|
+
activeList = null;
|
|
1201
1271
|
const rows = [parseMarkdownRow(line)];
|
|
1202
1272
|
index += 2;
|
|
1203
1273
|
while (index < lines.length && lines[index].includes("|")) {
|
|
@@ -1215,34 +1285,120 @@ function markdownToWikiDocument(markdown) {
|
|
|
1215
1285
|
blocks.push({
|
|
1216
1286
|
id: newWikiBlockId(),
|
|
1217
1287
|
type: "table",
|
|
1218
|
-
rows: rows.length,
|
|
1219
1288
|
cols: columnCount,
|
|
1289
|
+
rows: rows.length,
|
|
1290
|
+
colsWidth: Array.from({ length: columnCount }, (_, column) => Math.max(100, ...rows.map((row) => estimateWikiTableCellWidth(row[column] ?? "")))),
|
|
1220
1291
|
children
|
|
1221
1292
|
});
|
|
1222
1293
|
continue;
|
|
1223
1294
|
}
|
|
1224
1295
|
const heading = line.match(/^(#{1,6})[ \t]/);
|
|
1225
1296
|
if (heading?.[1]) {
|
|
1297
|
+
activeList = null;
|
|
1226
1298
|
blocks.push(wikiTextBlock(line.slice(heading[0].length).trim(), { heading: heading[1].length }));
|
|
1227
1299
|
continue;
|
|
1228
1300
|
}
|
|
1229
1301
|
const unordered = line.match(/^[ \t]*[-*+][ \t]/);
|
|
1230
1302
|
if (unordered) {
|
|
1231
|
-
|
|
1303
|
+
const groupId = activeList && !activeList.ordered ? activeList.groupId : newWikiBlockId();
|
|
1304
|
+
const start = activeList && !activeList.ordered ? activeList.nextStart : 1;
|
|
1305
|
+
activeList = {
|
|
1306
|
+
ordered: false,
|
|
1307
|
+
groupId,
|
|
1308
|
+
nextStart: start + 1
|
|
1309
|
+
};
|
|
1310
|
+
blocks.push(wikiTextBlock(line.slice(unordered[0].length).trim(), {
|
|
1311
|
+
list: true,
|
|
1312
|
+
groupId,
|
|
1313
|
+
start
|
|
1314
|
+
}));
|
|
1232
1315
|
continue;
|
|
1233
1316
|
}
|
|
1234
|
-
const ordered = line.match(/^[ \t]
|
|
1235
|
-
if (ordered) {
|
|
1317
|
+
const ordered = line.match(/^[ \t]*(\d+)[.)][ \t]/);
|
|
1318
|
+
if (ordered?.[1]) {
|
|
1319
|
+
const groupId = activeList?.ordered ? activeList.groupId : newWikiBlockId();
|
|
1320
|
+
const start = Number.parseInt(ordered[1], 10);
|
|
1321
|
+
activeList = {
|
|
1322
|
+
ordered: true,
|
|
1323
|
+
groupId,
|
|
1324
|
+
nextStart: start + 1
|
|
1325
|
+
};
|
|
1236
1326
|
blocks.push(wikiTextBlock(line.slice(ordered[0].length).trim(), {
|
|
1237
1327
|
list: true,
|
|
1238
|
-
ordered: true
|
|
1328
|
+
ordered: true,
|
|
1329
|
+
start,
|
|
1330
|
+
groupId
|
|
1239
1331
|
}));
|
|
1240
1332
|
continue;
|
|
1241
1333
|
}
|
|
1334
|
+
activeList = null;
|
|
1242
1335
|
blocks.push(wikiTextBlock(line));
|
|
1243
1336
|
}
|
|
1244
1337
|
return document;
|
|
1245
1338
|
}
|
|
1339
|
+
function markdownToWikiHtml(markdown) {
|
|
1340
|
+
const renderInline = (text) => markdownTextRuns(text).map((run) => {
|
|
1341
|
+
const escaped = escapeWikiHtml(run.insert);
|
|
1342
|
+
const content = run.attributes?.["style-code"] ? `<code>${escaped}</code>` : escaped;
|
|
1343
|
+
return run.attributes?.link ? `<a href="${escapeWikiHtml(run.attributes.link)}" target="_blank" rel="noopener noreferrer">${content}</a>` : content;
|
|
1344
|
+
}).join("");
|
|
1345
|
+
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
|
|
1346
|
+
const html = [];
|
|
1347
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
1348
|
+
const line = lines[index];
|
|
1349
|
+
if (!line.trim()) continue;
|
|
1350
|
+
if (line.includes("|") && lines[index + 1] && isMarkdownSeparatorRow(lines[index + 1])) {
|
|
1351
|
+
const rows = [parseMarkdownRow(line)];
|
|
1352
|
+
index += 2;
|
|
1353
|
+
while (index < lines.length && lines[index].includes("|")) {
|
|
1354
|
+
rows.push(parseMarkdownRow(lines[index]));
|
|
1355
|
+
index += 1;
|
|
1356
|
+
}
|
|
1357
|
+
index -= 1;
|
|
1358
|
+
const columnCount = Math.max(...rows.map((row) => row.length));
|
|
1359
|
+
const width = Math.floor(100 / columnCount);
|
|
1360
|
+
const header = Array.from({ length: columnCount }).map((_, column) => `<th style="width:${width}%">${renderInline(rows[0][column] ?? "")}</th>`).join("");
|
|
1361
|
+
const body = rows.slice(1).map((row) => `<tr>${Array.from({ length: columnCount }).map((_, column) => `<td>${renderInline(row[column] ?? "")}</td>`).join("")}</tr>`).join("");
|
|
1362
|
+
html.push(`<table style="width:100%"><thead><tr>${header}</tr></thead><tbody>${body}</tbody></table>`);
|
|
1363
|
+
continue;
|
|
1364
|
+
}
|
|
1365
|
+
const heading = line.match(/^(#{1,6})[ \t]/);
|
|
1366
|
+
if (heading?.[1]) {
|
|
1367
|
+
const level = heading[1].length;
|
|
1368
|
+
html.push(`<h${level}>${renderInline(line.slice(heading[0].length).trim())}</h${level}>`);
|
|
1369
|
+
continue;
|
|
1370
|
+
}
|
|
1371
|
+
if (line.match(/^[ \t]*(\d+)[.)][ \t]/)?.[1]) {
|
|
1372
|
+
const items = [];
|
|
1373
|
+
while (index < lines.length) {
|
|
1374
|
+
const item = lines[index].match(/^[ \t]*(\d+)[.)][ \t]/);
|
|
1375
|
+
if (!item?.[1]) break;
|
|
1376
|
+
items.push({
|
|
1377
|
+
value: Number.parseInt(item[1], 10),
|
|
1378
|
+
text: lines[index].slice(item[0].length).trim()
|
|
1379
|
+
});
|
|
1380
|
+
index += 1;
|
|
1381
|
+
}
|
|
1382
|
+
index -= 1;
|
|
1383
|
+
html.push(`<ol start="${items[0].value}">${items.map((item) => `<li>${renderInline(item.text)}</li>`).join("")}</ol>`);
|
|
1384
|
+
continue;
|
|
1385
|
+
}
|
|
1386
|
+
if (line.match(/^[ \t]*[-*+][ \t]/)) {
|
|
1387
|
+
const items = [];
|
|
1388
|
+
while (index < lines.length) {
|
|
1389
|
+
const item = lines[index].match(/^[ \t]*[-*+][ \t]/);
|
|
1390
|
+
if (!item) break;
|
|
1391
|
+
items.push(lines[index].slice(item[0].length).trim());
|
|
1392
|
+
index += 1;
|
|
1393
|
+
}
|
|
1394
|
+
index -= 1;
|
|
1395
|
+
html.push(`<ul>${items.map((item) => `<li>${renderInline(item)}</li>`).join("")}</ul>`);
|
|
1396
|
+
continue;
|
|
1397
|
+
}
|
|
1398
|
+
html.push(`<p>${renderInline(line.trim())}</p>`);
|
|
1399
|
+
}
|
|
1400
|
+
return html.join("\n");
|
|
1401
|
+
}
|
|
1246
1402
|
function parseWikiDocument(content) {
|
|
1247
1403
|
const document = parseJsonRecord(content);
|
|
1248
1404
|
if (!document || !Array.isArray(document.blocks)) throw new Error("ONES: Wiki content is not a supported collaborative document");
|
|
@@ -1299,6 +1455,17 @@ function appendWikiTableRow(document, operation) {
|
|
|
1299
1455
|
table.rows = (typeof table.rows === "number" ? table.rows : layout.rows.length) + 1;
|
|
1300
1456
|
}
|
|
1301
1457
|
function applyWikiUpdateOperation(content, operation) {
|
|
1458
|
+
if (operation.type === "replace_document") {
|
|
1459
|
+
const current = parseWikiDocument(content);
|
|
1460
|
+
const replacement = markdownToWikiDocument(operation.markdown);
|
|
1461
|
+
for (const key of [
|
|
1462
|
+
"comments",
|
|
1463
|
+
"meta",
|
|
1464
|
+
"authors",
|
|
1465
|
+
"commentators"
|
|
1466
|
+
]) if (Object.hasOwn(current, key)) replacement[key] = current[key];
|
|
1467
|
+
return JSON.stringify(replacement);
|
|
1468
|
+
}
|
|
1302
1469
|
const document = parseWikiDocument(content);
|
|
1303
1470
|
if (operation.type === "append_blocks") appendWikiDocument(document, markdownToWikiDocument(operation.markdown));
|
|
1304
1471
|
else if (operation.type === "replace_text") replaceWikiText(document, operation.find, operation.replace);
|
|
@@ -1337,6 +1504,30 @@ var WikiPathResolutionError = class extends Error {
|
|
|
1337
1504
|
};
|
|
1338
1505
|
//#endregion
|
|
1339
1506
|
//#region ../../src/adapters/ones/wiki-reader.ts
|
|
1507
|
+
function isWikiAttachmentPath(source) {
|
|
1508
|
+
if (/^[a-z][a-z\d+.-]*:/i.test(source)) return false;
|
|
1509
|
+
return source.split("/").every((part) => Boolean(part) && part !== "." && part !== ".." && !part.includes("\\"));
|
|
1510
|
+
}
|
|
1511
|
+
const CURRENT_USER_PATH_ALIASES = {
|
|
1512
|
+
"i": true,
|
|
1513
|
+
"me": true,
|
|
1514
|
+
"mine": true,
|
|
1515
|
+
"my": true,
|
|
1516
|
+
"myself": true,
|
|
1517
|
+
"current user": true,
|
|
1518
|
+
"current-user": true,
|
|
1519
|
+
"current_user": true,
|
|
1520
|
+
"我": true,
|
|
1521
|
+
"我的": true,
|
|
1522
|
+
"本人": true,
|
|
1523
|
+
"当前用户": true,
|
|
1524
|
+
"当前账号": true,
|
|
1525
|
+
"自己": true
|
|
1526
|
+
};
|
|
1527
|
+
function currentUserPathAlias(segment) {
|
|
1528
|
+
const trimmed = segment.trim();
|
|
1529
|
+
return CURRENT_USER_PATH_ALIASES[trimmed.toLocaleLowerCase()] ? trimmed : null;
|
|
1530
|
+
}
|
|
1340
1531
|
var OnesWikiOpenApiError = class extends Error {
|
|
1341
1532
|
status;
|
|
1342
1533
|
reason;
|
|
@@ -1431,12 +1622,29 @@ function segmentSimilarity(left, right) {
|
|
|
1431
1622
|
var OnesWikiReader = class {
|
|
1432
1623
|
options;
|
|
1433
1624
|
treeCache = /* @__PURE__ */ new Map();
|
|
1625
|
+
currentUserNameRequest = null;
|
|
1434
1626
|
constructor(options) {
|
|
1435
1627
|
this.options = options;
|
|
1436
1628
|
}
|
|
1437
1629
|
invalidateTree(teamId, spaceId) {
|
|
1438
1630
|
this.treeCache.delete(`${teamId}:${spaceId}`);
|
|
1439
1631
|
}
|
|
1632
|
+
async fetchCurrentUserName() {
|
|
1633
|
+
const session = await this.options.getSession();
|
|
1634
|
+
const response = await fetch(new URL("/wiki/api/project/auth/token_info", this.options.apiBase).toString(), { headers: { Authorization: `Bearer ${session.accessToken}` } });
|
|
1635
|
+
if (!response.ok) throw new Error(`ONES Wiki token info error: ${response.status}`);
|
|
1636
|
+
const payload = await response.json();
|
|
1637
|
+
const name = firstNonEmptyString$1(payload.user?.name, payload.data?.user?.name, payload.data?.name, payload.name, payload.user_name, payload.userName);
|
|
1638
|
+
if (!name) throw new Error("ONES Wiki token info did not include a resolvable current account");
|
|
1639
|
+
return name;
|
|
1640
|
+
}
|
|
1641
|
+
currentUserName() {
|
|
1642
|
+
if (!this.currentUserNameRequest) this.currentUserNameRequest = this.fetchCurrentUserName().catch((error) => {
|
|
1643
|
+
this.currentUserNameRequest = null;
|
|
1644
|
+
throw error;
|
|
1645
|
+
});
|
|
1646
|
+
return this.currentUserNameRequest;
|
|
1647
|
+
}
|
|
1440
1648
|
async openApi(apiPath) {
|
|
1441
1649
|
if (!apiPath.startsWith("/wiki/")) throw new Error("ONES: Invalid Wiki Open API path");
|
|
1442
1650
|
if (!this.options.openApiToken) throw new OnesWikiOpenApiError(null, "ONES Wiki Open API credential is not configured. Set source.openApiAuth.tokenEnv to a Personal API Key with read scopes.", "missing_credential");
|
|
@@ -1462,9 +1670,8 @@ var OnesWikiReader = class {
|
|
|
1462
1670
|
}
|
|
1463
1671
|
buildImageUrl(session, refUuid, source, token, teamUuid) {
|
|
1464
1672
|
const encodedRefUuid = encodeIdentifier$1(refUuid, "wiki reference UUID");
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
const encodedSource = sourceParts.map((part) => encodeURIComponent(part)).join("/");
|
|
1673
|
+
if (!isWikiAttachmentPath(source)) throw new Error("ONES: Invalid wiki attachment path");
|
|
1674
|
+
const encodedSource = source.split("/").map((part) => encodeURIComponent(part)).join("/");
|
|
1468
1675
|
const encodedTeamUuid = encodeIdentifier$1(teamUuid ?? session.teamUuid, "team UUID");
|
|
1469
1676
|
return `${this.options.apiBase}/wiki/api/wiki/editor/${encodedTeamUuid}/${encodedRefUuid}/resources/${encodedSource}?token=${encodeURIComponent(token)}`;
|
|
1470
1677
|
}
|
|
@@ -1482,7 +1689,8 @@ var OnesWikiReader = class {
|
|
|
1482
1689
|
const context = { imageSources: [] };
|
|
1483
1690
|
const content = renderWikiContent(typeof data.content === "string" ? data.content : "", context);
|
|
1484
1691
|
const token = typeof data.token === "string" ? data.token : "";
|
|
1485
|
-
|
|
1692
|
+
const attachmentSources = context.imageSources.filter(isWikiAttachmentPath);
|
|
1693
|
+
if (!attachmentSources.length || !token) return {
|
|
1486
1694
|
content,
|
|
1487
1695
|
attachments: []
|
|
1488
1696
|
};
|
|
@@ -1494,7 +1702,7 @@ var OnesWikiReader = class {
|
|
|
1494
1702
|
};
|
|
1495
1703
|
return {
|
|
1496
1704
|
content,
|
|
1497
|
-
attachments:
|
|
1705
|
+
attachments: attachmentSources.map((source, index) => ({
|
|
1498
1706
|
id: `${wikiUuid}-image-${index + 1}`,
|
|
1499
1707
|
name: attachmentNameFromPath(source),
|
|
1500
1708
|
url: this.buildImageUrl(session, refUuid, source, token, wikiTeamUuid),
|
|
@@ -1696,8 +1904,19 @@ var OnesWikiReader = class {
|
|
|
1696
1904
|
return score >= MIN_PATH_MATCH_SIMILARITY ? score : null;
|
|
1697
1905
|
}
|
|
1698
1906
|
async resolvePath(params) {
|
|
1699
|
-
const
|
|
1700
|
-
if (!
|
|
1907
|
+
const requestedPath = params.path.map((segment) => segment.trim()).filter(Boolean);
|
|
1908
|
+
if (!requestedPath.length) throw new Error("ONES: Wiki path is empty");
|
|
1909
|
+
const currentUserAlias = requestedPath.map(currentUserPathAlias).find((alias) => alias !== null) ?? null;
|
|
1910
|
+
const currentUserName = currentUserAlias ? await this.currentUserName() : null;
|
|
1911
|
+
const path = currentUserName ? requestedPath.map((segment) => currentUserPathAlias(segment) ? currentUserName : segment) : requestedPath;
|
|
1912
|
+
const redactCurrentUser = (page) => {
|
|
1913
|
+
if (!currentUserName || !currentUserAlias) return page;
|
|
1914
|
+
return {
|
|
1915
|
+
...page,
|
|
1916
|
+
title: page.title === currentUserName ? currentUserAlias : page.title,
|
|
1917
|
+
breadcrumb: page.breadcrumb.map((segment) => segment === currentUserName ? currentUserAlias : segment)
|
|
1918
|
+
};
|
|
1919
|
+
};
|
|
1701
1920
|
const candidates = await this.search({
|
|
1702
1921
|
query: path.at(-1),
|
|
1703
1922
|
teamId: params.teamId,
|
|
@@ -1744,26 +1963,32 @@ var OnesWikiReader = class {
|
|
|
1744
1963
|
});
|
|
1745
1964
|
}
|
|
1746
1965
|
}
|
|
1747
|
-
if (matches.length > 1) throw new WikiPathResolutionError("ambiguous",
|
|
1966
|
+
if (matches.length > 1) throw new WikiPathResolutionError("ambiguous", requestedPath, matches.map(redactCurrentUser));
|
|
1748
1967
|
let match = matches[0];
|
|
1749
1968
|
if (!match && fuzzy.length) {
|
|
1750
1969
|
fuzzy.sort((left, right) => right.score - left.score);
|
|
1751
1970
|
const competing = fuzzy.filter((candidate) => fuzzy[0].score - candidate.score < PATH_MATCH_MARGIN);
|
|
1752
|
-
if (competing.length > 1) throw new WikiPathResolutionError("ambiguous",
|
|
1971
|
+
if (competing.length > 1) throw new WikiPathResolutionError("ambiguous", requestedPath, competing.map((candidate) => redactCurrentUser(candidate.page)));
|
|
1753
1972
|
match = fuzzy[0].page;
|
|
1754
1973
|
}
|
|
1755
1974
|
if (!match) {
|
|
1756
1975
|
const inspectedIds = new Set(inspected.map((page) => page.pageId));
|
|
1757
|
-
throw new WikiPathResolutionError("not_found",
|
|
1976
|
+
throw new WikiPathResolutionError("not_found", requestedPath, [...inspected, ...candidates.filter((page) => !inspectedIds.has(page.pageId))].map(redactCurrentUser));
|
|
1758
1977
|
}
|
|
1759
1978
|
if (!match.spaceId) throw new Error("ONES: Wiki space ID could not be verified");
|
|
1760
|
-
|
|
1979
|
+
const publicMatch = redactCurrentUser(match);
|
|
1980
|
+
const resolution = {
|
|
1761
1981
|
teamId: match.teamId,
|
|
1762
1982
|
spaceId: match.spaceId,
|
|
1763
1983
|
pageId: match.pageId,
|
|
1764
|
-
title:
|
|
1765
|
-
breadcrumb:
|
|
1984
|
+
title: publicMatch.title,
|
|
1985
|
+
breadcrumb: publicMatch.breadcrumb
|
|
1766
1986
|
};
|
|
1987
|
+
if (currentUserName && currentUserAlias) Object.defineProperty(resolution, "redactPrivateValues", {
|
|
1988
|
+
enumerable: false,
|
|
1989
|
+
value: (value) => value.split(currentUserName).join(currentUserAlias)
|
|
1990
|
+
});
|
|
1991
|
+
return resolution;
|
|
1767
1992
|
}
|
|
1768
1993
|
};
|
|
1769
1994
|
//#endregion
|
|
@@ -1965,8 +2190,9 @@ var OnesTaskContent = class {
|
|
|
1965
2190
|
attachments: rendered.attachments
|
|
1966
2191
|
};
|
|
1967
2192
|
})), containsInlineTaskImages(task) ? this.getTaskImageAttachments(task) : Promise.resolve([])]);
|
|
2193
|
+
const projectIdentifier = task.project?.identifier?.toUpperCase() ?? null;
|
|
1968
2194
|
const parts = [
|
|
1969
|
-
`#
|
|
2195
|
+
`# ${taskDisplayId({}, task, projectIdentifier)} ${task.name}`,
|
|
1970
2196
|
"",
|
|
1971
2197
|
`- **Type**: ${task.issueType?.name ?? "Unknown"}`,
|
|
1972
2198
|
"- **Work Item Kind**: requirement",
|
|
@@ -1978,7 +2204,7 @@ var OnesTaskContent = class {
|
|
|
1978
2204
|
parts.push(`- **UUID**: ${task.uuid}`);
|
|
1979
2205
|
if (task.relatedTasks?.length) {
|
|
1980
2206
|
parts.push("", "## Related Tasks");
|
|
1981
|
-
for (const related of task.relatedTasks) parts.push(`-
|
|
2207
|
+
for (const related of task.relatedTasks) parts.push(`- ${taskDisplayId({}, related, related.project?.identifier?.toUpperCase() ?? projectIdentifier)} ${related.name} [${related.issueType?.name}] (${related.status?.name}) — ${related.assign?.name ?? "Unassigned"}`);
|
|
1982
2208
|
}
|
|
1983
2209
|
if (relatedActivities.length) {
|
|
1984
2210
|
parts.push("", "## Related Work Items");
|
|
@@ -1993,7 +2219,7 @@ var OnesTaskContent = class {
|
|
|
1993
2219
|
}
|
|
1994
2220
|
if (task.parent?.uuid) {
|
|
1995
2221
|
parts.push("", "## Parent Task", `- UUID: ${task.parent.uuid}`);
|
|
1996
|
-
if (task.parent.number) parts.push(`- Number:
|
|
2222
|
+
if (task.parent.number) parts.push(`- Number: ${projectIdentifier ? `${projectIdentifier}-${task.parent.number}` : `#${task.parent.number}`}`);
|
|
1997
2223
|
}
|
|
1998
2224
|
if (wikiContents.length > 0) {
|
|
1999
2225
|
parts.push("", "---", "", "## Requirement Documents");
|
|
@@ -2016,8 +2242,9 @@ var OnesTaskContent = class {
|
|
|
2016
2242
|
}
|
|
2017
2243
|
buildWorkItemSummary(task, kind) {
|
|
2018
2244
|
const nextTool = kind === "defect" ? "get_issue_detail" : "get_related_issues / get_testcases";
|
|
2245
|
+
const projectIdentifier = task.project?.identifier?.toUpperCase() ?? null;
|
|
2019
2246
|
const parts = [
|
|
2020
|
-
`#
|
|
2247
|
+
`# ${taskDisplayId({}, task, projectIdentifier)} ${task.name}`,
|
|
2021
2248
|
"",
|
|
2022
2249
|
`- **Type**: ${task.subIssueType?.name ?? task.issueType?.name ?? "Unknown"}`,
|
|
2023
2250
|
`- **Work Item Kind**: ${kind}`,
|
|
@@ -2029,14 +2256,14 @@ var OnesTaskContent = class {
|
|
|
2029
2256
|
parts.push(`- **UUID**: ${task.uuid}`);
|
|
2030
2257
|
if (task.parent?.uuid) {
|
|
2031
2258
|
parts.push("", "## Parent Task", `- UUID: ${task.parent.uuid}`);
|
|
2032
|
-
if (task.parent.number) parts.push(`- Number:
|
|
2259
|
+
if (task.parent.number) parts.push(`- Number: ${projectIdentifier ? `${projectIdentifier}-${task.parent.number}` : `#${task.parent.number}`}`);
|
|
2033
2260
|
}
|
|
2034
2261
|
const detailText = getTaskDetailText(task);
|
|
2035
2262
|
if (detailText) parts.push("", "---", "", kind === "defect" ? "## Defect Detail" : "## Task Detail", "", detailText);
|
|
2036
2263
|
parts.push("", "## Next Tool", "", `This ID is a ${workItemKindLabel(kind)}, not a requirement document.`, `Do not treat wiki/requirement docs as the source of truth. Use \`${nextTool}\` for the next lookup.`);
|
|
2037
2264
|
if (task.relatedTasks?.length) {
|
|
2038
2265
|
parts.push("", "## Related Tasks");
|
|
2039
|
-
for (const related of task.relatedTasks) parts.push(`-
|
|
2266
|
+
for (const related of task.relatedTasks) parts.push(`- ${taskDisplayId({}, related, related.project?.identifier?.toUpperCase() ?? projectIdentifier)} ${related.name} [${related.issueType?.name}] (${related.status?.name}) — ${related.assign?.name ?? "Unassigned"}`);
|
|
2040
2267
|
}
|
|
2041
2268
|
const requirement = toRequirement(task, parts.join("\n"));
|
|
2042
2269
|
requirement.raw = {
|
|
@@ -2098,7 +2325,7 @@ const TASK_DETAIL_QUERY = `
|
|
|
2098
2325
|
priority { value }
|
|
2099
2326
|
assign { uuid name }
|
|
2100
2327
|
owner { uuid name }
|
|
2101
|
-
project { uuid name }
|
|
2328
|
+
project { uuid name identifier }
|
|
2102
2329
|
parent { uuid number issueType { uuid name } }
|
|
2103
2330
|
relatedTasks {
|
|
2104
2331
|
key uuid number name
|
|
@@ -2109,6 +2336,7 @@ const TASK_DETAIL_QUERY = `
|
|
|
2109
2336
|
subIssueType { uuid name detailType }
|
|
2110
2337
|
status { uuid name category }
|
|
2111
2338
|
assign { uuid name }
|
|
2339
|
+
project { uuid name identifier }
|
|
2112
2340
|
}
|
|
2113
2341
|
relatedWikiPages {
|
|
2114
2342
|
uuid title referenceType subReferenceType errorMessage
|
|
@@ -2245,7 +2473,9 @@ var OnesTaskPlanning = class {
|
|
|
2245
2473
|
if (!Number.isInteger(raw.number)) throw new TypeError("ONES: Standalone wiki pages cannot be decomposed into requirement tasks");
|
|
2246
2474
|
const parsedDisplayId = parseDisplayId(params.requirementId);
|
|
2247
2475
|
const requirementInfo = await this.options.fetchTaskInfo(workItem.id);
|
|
2248
|
-
|
|
2476
|
+
let projectIdentifier = parsedDisplayId?.identifier ?? firstString(requirementInfo, ["projectIdentifier", "project_identifier"]) ?? raw.project?.identifier ?? null;
|
|
2477
|
+
if (!projectIdentifier && raw.project?.uuid) projectIdentifier = await this.options.resolveProjectIdentifier(raw.project.uuid);
|
|
2478
|
+
projectIdentifier = projectIdentifier?.toUpperCase() ?? null;
|
|
2249
2479
|
const displayId = firstString(requirementInfo, ["displayId", "display_id"]) ?? (projectIdentifier ? `${projectIdentifier}-${raw.number}` : `#${raw.number}`);
|
|
2250
2480
|
const relatedTasks = (raw.relatedTasks ?? []).filter((task) => classifyOnesWorkItem(task.issueType, task.subIssueType) === "task");
|
|
2251
2481
|
const relatedInfos = await Promise.all(relatedTasks.map((task) => this.options.fetchTaskInfo(task.uuid)));
|
|
@@ -2574,6 +2804,12 @@ var OnesTestcaseReader = class {
|
|
|
2574
2804
|
const JSON0_URI = "http://sharejs.org/types/JSONv0";
|
|
2575
2805
|
const JSON1_URI = "http://sharejs.org/types/JSONv1";
|
|
2576
2806
|
const DEFAULT_TIMEOUT_MS$1 = 15e3;
|
|
2807
|
+
function getSetCookies(response) {
|
|
2808
|
+
const headers = response.headers;
|
|
2809
|
+
if (headers.getSetCookie) return headers.getSetCookie();
|
|
2810
|
+
const raw = headers.get("set-cookie");
|
|
2811
|
+
return raw ? [raw] : [];
|
|
2812
|
+
}
|
|
2577
2813
|
function isRecord(value) {
|
|
2578
2814
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
2579
2815
|
}
|
|
@@ -2586,28 +2822,44 @@ function asDoc(value) {
|
|
|
2586
2822
|
}
|
|
2587
2823
|
function createTopLevelJson1Operation(current, next) {
|
|
2588
2824
|
let operation = null;
|
|
2825
|
+
const appendComponent = (component) => {
|
|
2826
|
+
operation = operation === null ? component : type.compose(operation, component);
|
|
2827
|
+
};
|
|
2589
2828
|
const keys = [.../* @__PURE__ */ new Set([...Object.keys(current), ...Object.keys(next)])].sort();
|
|
2590
2829
|
for (const key of keys) {
|
|
2830
|
+
if (key === "blocks") continue;
|
|
2591
2831
|
const hasCurrent = Object.hasOwn(current, key);
|
|
2592
2832
|
const hasNext = Object.hasOwn(next, key);
|
|
2593
|
-
|
|
2594
|
-
if (
|
|
2595
|
-
else if (!
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2833
|
+
if (!hasCurrent && hasNext) appendComponent(insertOp([key], asDoc(next[key])));
|
|
2834
|
+
else if (hasCurrent && !hasNext) appendComponent(removeOp([key]));
|
|
2835
|
+
else if (hasCurrent && hasNext && !isDeepStrictEqual(current[key], next[key])) appendComponent(replaceOp([key], asDoc(current[key]), asDoc(next[key])));
|
|
2836
|
+
}
|
|
2837
|
+
const currentBlocks = Array.isArray(current.blocks) ? current.blocks : [];
|
|
2838
|
+
const nextBlocks = Array.isArray(next.blocks) ? next.blocks : [];
|
|
2839
|
+
const sharedBlockCount = Math.min(currentBlocks.length, nextBlocks.length);
|
|
2840
|
+
for (let index = 0; index < sharedBlockCount; index += 1) if (!isDeepStrictEqual(currentBlocks[index], nextBlocks[index])) {
|
|
2841
|
+
appendComponent(removeOp(["blocks", index]));
|
|
2842
|
+
appendComponent(insertOp(["blocks", index], asDoc(nextBlocks[index])));
|
|
2843
|
+
}
|
|
2844
|
+
for (let index = currentBlocks.length - 1; index >= nextBlocks.length; index -= 1) appendComponent(removeOp(["blocks", index]));
|
|
2845
|
+
for (let index = currentBlocks.length; index < nextBlocks.length; index += 1) appendComponent(insertOp(["blocks", index], asDoc(nextBlocks[index])));
|
|
2600
2846
|
if (operation !== null) type.checkValidOp(operation);
|
|
2601
2847
|
return operation;
|
|
2602
2848
|
}
|
|
2849
|
+
function createTopLevelJson1Operations(current, next) {
|
|
2850
|
+
const operation = createTopLevelJson1Operation(current, next);
|
|
2851
|
+
return operation === null ? [] : [operation];
|
|
2852
|
+
}
|
|
2603
2853
|
function wikiEditorUrls(baseUrl, teamId, documentId) {
|
|
2604
2854
|
const url = new URL(baseUrl);
|
|
2605
2855
|
if (url.protocol !== "https:" && url.protocol !== "http:") throw new Error("ONES Wiki collaboration requires an HTTP(S) source URL");
|
|
2606
2856
|
const path = `/wiki/api/wiki/editor/${encodeURIComponent(teamId)}/${encodeURIComponent(documentId)}`;
|
|
2607
|
-
const
|
|
2857
|
+
const editorBaseUrl = new URL(path, url);
|
|
2858
|
+
const socketUrl = new URL(editorBaseUrl);
|
|
2608
2859
|
socketUrl.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
2609
2860
|
return {
|
|
2610
|
-
authUrl:
|
|
2861
|
+
authUrl: `${editorBaseUrl.toString()}/auth`,
|
|
2862
|
+
editorBaseUrl: editorBaseUrl.toString(),
|
|
2611
2863
|
socketUrl: socketUrl.toString()
|
|
2612
2864
|
};
|
|
2613
2865
|
}
|
|
@@ -2624,39 +2876,52 @@ function closeSocket(socket) {
|
|
|
2624
2876
|
}
|
|
2625
2877
|
async function replaceOnesWikiDocument(options, update, dependencies = {
|
|
2626
2878
|
fetch,
|
|
2627
|
-
openWebSocket: (url) => new WebSocket(url)
|
|
2879
|
+
openWebSocket: (url, headers) => new WebSocket(url, { headers })
|
|
2628
2880
|
}) {
|
|
2629
|
-
const { authUrl, socketUrl } = wikiEditorUrls(options.baseUrl, options.teamId, options.documentId);
|
|
2881
|
+
const { authUrl, editorBaseUrl, socketUrl } = wikiEditorUrls(options.baseUrl, options.teamId, options.documentId);
|
|
2630
2882
|
const authResponse = await dependencies.fetch(authUrl, {
|
|
2631
2883
|
method: "GET",
|
|
2632
2884
|
headers: {
|
|
2633
2885
|
"Authorization": `Bearer ${options.accessToken}`,
|
|
2634
2886
|
"x-live-editor-token": options.editorToken,
|
|
2635
|
-
"x-live-editor-base-url": Buffer.from(
|
|
2887
|
+
"x-live-editor-base-url": Buffer.from(editorBaseUrl).toString("base64url"),
|
|
2888
|
+
...options.cookieHeader ? { Cookie: options.cookieHeader } : {}
|
|
2636
2889
|
}
|
|
2637
2890
|
});
|
|
2638
2891
|
if (!authResponse.ok) throw new Error(`ONES Wiki collaboration auth failed with status ${authResponse.status}`);
|
|
2639
2892
|
const editorAuth = await authResponse.json();
|
|
2640
2893
|
if (typeof editorAuth.read !== "string" || !editorAuth.read) throw new Error("ONES Wiki collaboration auth response did not include a read token");
|
|
2894
|
+
const cookies = [options.cookieHeader, ...getSetCookies(authResponse).map((cookie) => cookie.split(";")[0])].filter((cookie) => Boolean(cookie)).join("; ");
|
|
2895
|
+
const socketHeaders = {
|
|
2896
|
+
"Accept-Language": "zh-CN,zh;q=0.9",
|
|
2897
|
+
"Cache-Control": "no-cache",
|
|
2898
|
+
"Pragma": "no-cache",
|
|
2899
|
+
"User-Agent": "Mozilla/5.0 AppleWebKit/537.36 Chrome/128.0.0.0 Safari/537.36",
|
|
2900
|
+
"Origin": new URL(options.baseUrl).origin,
|
|
2901
|
+
...cookies ? { Cookie: cookies } : {}
|
|
2902
|
+
};
|
|
2641
2903
|
return new Promise((resolve, reject) => {
|
|
2642
|
-
const socket = dependencies.openWebSocket(socketUrl);
|
|
2643
|
-
|
|
2644
|
-
let clientId = "";
|
|
2904
|
+
const socket = dependencies.openWebSocket(socketUrl, socketHeaders);
|
|
2905
|
+
let sequence = 1;
|
|
2645
2906
|
let state = "init";
|
|
2646
2907
|
let settled = false;
|
|
2647
2908
|
let snapshotVersion = 0;
|
|
2909
|
+
let currentVersion = 0;
|
|
2910
|
+
let pendingOperations = [];
|
|
2911
|
+
const presenceChannel = `${options.teamId}:${options.documentId}`;
|
|
2912
|
+
const presenceId = randomBytes(7).toString("base64url").slice(0, 9);
|
|
2648
2913
|
let timeout;
|
|
2649
2914
|
const finish = (result) => {
|
|
2650
2915
|
if (settled) return;
|
|
2651
2916
|
settled = true;
|
|
2652
|
-
|
|
2917
|
+
clearTimeout(timeout);
|
|
2653
2918
|
resolve(result);
|
|
2654
2919
|
closeSocket(socket);
|
|
2655
2920
|
};
|
|
2656
2921
|
const fail = (error) => {
|
|
2657
2922
|
if (settled) return;
|
|
2658
2923
|
settled = true;
|
|
2659
|
-
|
|
2924
|
+
clearTimeout(timeout);
|
|
2660
2925
|
reject(error);
|
|
2661
2926
|
closeSocket(socket);
|
|
2662
2927
|
};
|
|
@@ -2665,9 +2930,38 @@ async function replaceOnesWikiDocument(options, update, dependencies = {
|
|
|
2665
2930
|
if (error) fail(/* @__PURE__ */ new Error(`ONES Wiki collaboration send failed: ${error.message}`));
|
|
2666
2931
|
});
|
|
2667
2932
|
};
|
|
2933
|
+
const sendHandshake = () => {
|
|
2934
|
+
send({
|
|
2935
|
+
a: "hs",
|
|
2936
|
+
id: null,
|
|
2937
|
+
auth: {
|
|
2938
|
+
appId: options.teamId,
|
|
2939
|
+
docId: options.documentId,
|
|
2940
|
+
userId: options.userId,
|
|
2941
|
+
permission: "w",
|
|
2942
|
+
token: options.editorToken,
|
|
2943
|
+
displayName: options.displayName ?? "",
|
|
2944
|
+
avatarUrl: options.avatarUrl ?? ""
|
|
2945
|
+
}
|
|
2946
|
+
});
|
|
2947
|
+
};
|
|
2948
|
+
const sendNextOperation = () => {
|
|
2949
|
+
const operation = pendingOperations[0];
|
|
2950
|
+
if (operation === void 0) return;
|
|
2951
|
+
send({
|
|
2952
|
+
a: "op",
|
|
2953
|
+
c: options.teamId,
|
|
2954
|
+
d: options.documentId,
|
|
2955
|
+
v: currentVersion,
|
|
2956
|
+
seq: sequence,
|
|
2957
|
+
x: {},
|
|
2958
|
+
op: operation
|
|
2959
|
+
});
|
|
2960
|
+
};
|
|
2668
2961
|
timeout = setTimeout(() => {
|
|
2669
2962
|
fail(/* @__PURE__ */ new Error(`ONES Wiki collaboration timed out while waiting for ${state}`));
|
|
2670
2963
|
}, options.timeoutMs ?? DEFAULT_TIMEOUT_MS$1);
|
|
2964
|
+
socket.on("open", sendHandshake);
|
|
2671
2965
|
socket.on("error", () => fail(/* @__PURE__ */ new Error("ONES Wiki collaboration WebSocket failed")));
|
|
2672
2966
|
socket.on("close", (code) => {
|
|
2673
2967
|
if (!settled) fail(/* @__PURE__ */ new Error(`ONES Wiki collaboration WebSocket closed before ${state} (code ${code})`));
|
|
@@ -2694,58 +2988,64 @@ async function replaceOnesWikiDocument(options, update, dependencies = {
|
|
|
2694
2988
|
fail(/* @__PURE__ */ new Error("ONES Wiki collaboration returned an unsupported init frame"));
|
|
2695
2989
|
return;
|
|
2696
2990
|
}
|
|
2697
|
-
clientId = message.id;
|
|
2698
2991
|
state = "handshake";
|
|
2699
|
-
|
|
2700
|
-
a: "hs",
|
|
2701
|
-
id: clientId,
|
|
2702
|
-
auth: {
|
|
2703
|
-
appId: options.teamId,
|
|
2704
|
-
docId: options.documentId,
|
|
2705
|
-
userId: options.userId,
|
|
2706
|
-
permission: "w",
|
|
2707
|
-
token: editorAuth.read,
|
|
2708
|
-
displayName: options.displayName ?? "",
|
|
2709
|
-
avatarUrl: options.avatarUrl ?? ""
|
|
2710
|
-
},
|
|
2711
|
-
options: { ping: {
|
|
2712
|
-
interval: 5e4,
|
|
2713
|
-
timeout: 15e4
|
|
2714
|
-
} }
|
|
2715
|
-
});
|
|
2992
|
+
sendHandshake();
|
|
2716
2993
|
return;
|
|
2717
2994
|
}
|
|
2718
2995
|
if (state === "handshake") {
|
|
2719
|
-
if (message.a !== "hs" || message.id !==
|
|
2996
|
+
if (message.a !== "hs" || typeof message.id !== "string" || message.protocol !== 1 || message.protocolMinor !== 1 || message.type !== JSON0_URI) {
|
|
2720
2997
|
fail(/* @__PURE__ */ new Error("ONES Wiki collaboration returned an unsupported handshake frame"));
|
|
2721
2998
|
return;
|
|
2722
2999
|
}
|
|
2723
|
-
state = "
|
|
3000
|
+
state = "fetch";
|
|
2724
3001
|
send({
|
|
2725
|
-
a: "
|
|
3002
|
+
a: "f",
|
|
2726
3003
|
c: options.teamId,
|
|
2727
3004
|
d: options.documentId
|
|
2728
3005
|
});
|
|
2729
3006
|
return;
|
|
2730
3007
|
}
|
|
2731
|
-
if (state === "
|
|
3008
|
+
if (state === "fetch") {
|
|
2732
3009
|
const snapshot = message.data;
|
|
2733
|
-
if (message.a !== "
|
|
2734
|
-
fail(/* @__PURE__ */ new Error("ONES Wiki collaboration returned an unsupported snapshot frame"));
|
|
2735
|
-
return;
|
|
2736
|
-
}
|
|
3010
|
+
if (message.a !== "f" || message.c !== options.teamId || message.d !== options.documentId || typeof snapshot?.v !== "number" || snapshot.type !== JSON1_URI) return;
|
|
2737
3011
|
const current = asWikiSnapshot(snapshot.data);
|
|
2738
|
-
let next;
|
|
2739
|
-
let operation;
|
|
2740
3012
|
try {
|
|
2741
|
-
|
|
2742
|
-
operation = createTopLevelJson1Operation(current, next);
|
|
3013
|
+
pendingOperations = createTopLevelJson1Operations(current, asJsonDocument(update(structuredClone(current)), "update"));
|
|
2743
3014
|
} catch (error) {
|
|
2744
3015
|
fail(error instanceof Error ? error : /* @__PURE__ */ new Error("ONES Wiki collaboration update failed"));
|
|
2745
3016
|
return;
|
|
2746
3017
|
}
|
|
2747
3018
|
snapshotVersion = snapshot.v;
|
|
2748
|
-
|
|
3019
|
+
currentVersion = snapshotVersion;
|
|
3020
|
+
state = "presence";
|
|
3021
|
+
send({
|
|
3022
|
+
a: "p",
|
|
3023
|
+
ch: presenceChannel,
|
|
3024
|
+
id: presenceId,
|
|
3025
|
+
p: null,
|
|
3026
|
+
pv: 2
|
|
3027
|
+
});
|
|
3028
|
+
send({
|
|
3029
|
+
a: "ps",
|
|
3030
|
+
ch: presenceChannel,
|
|
3031
|
+
seq: 1
|
|
3032
|
+
});
|
|
3033
|
+
return;
|
|
3034
|
+
}
|
|
3035
|
+
if (state === "presence") {
|
|
3036
|
+
if (message.a !== "ps" || message.ch !== presenceChannel || message.seq !== 1) return;
|
|
3037
|
+
state = "subscribe";
|
|
3038
|
+
send({
|
|
3039
|
+
a: "s",
|
|
3040
|
+
c: options.teamId,
|
|
3041
|
+
d: options.documentId,
|
|
3042
|
+
v: snapshotVersion
|
|
3043
|
+
});
|
|
3044
|
+
return;
|
|
3045
|
+
}
|
|
3046
|
+
if (state === "subscribe") {
|
|
3047
|
+
if (message.a !== "s" || message.c !== options.teamId || message.d !== options.documentId) return;
|
|
3048
|
+
if (!pendingOperations.length) {
|
|
2749
3049
|
finish({
|
|
2750
3050
|
snapshotVersion,
|
|
2751
3051
|
version: snapshotVersion,
|
|
@@ -2754,21 +3054,23 @@ async function replaceOnesWikiDocument(options, update, dependencies = {
|
|
|
2754
3054
|
return;
|
|
2755
3055
|
}
|
|
2756
3056
|
state = "ack";
|
|
2757
|
-
|
|
2758
|
-
a: "op",
|
|
2759
|
-
c: options.teamId,
|
|
2760
|
-
d: options.documentId,
|
|
2761
|
-
v: snapshotVersion,
|
|
2762
|
-
seq: sequence,
|
|
2763
|
-
op: operation
|
|
2764
|
-
});
|
|
3057
|
+
sendNextOperation();
|
|
2765
3058
|
return;
|
|
2766
3059
|
}
|
|
2767
|
-
if (state === "ack" && message.a === "op" && message.c === options.teamId && message.d === options.documentId && message.seq === sequence)
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
3060
|
+
if (state === "ack" && message.a === "op" && message.c === options.teamId && message.d === options.documentId && message.seq === sequence) {
|
|
3061
|
+
currentVersion = typeof message.v === "number" ? message.v : currentVersion + 1;
|
|
3062
|
+
pendingOperations.shift();
|
|
3063
|
+
if (!pendingOperations.length) {
|
|
3064
|
+
finish({
|
|
3065
|
+
snapshotVersion,
|
|
3066
|
+
version: currentVersion,
|
|
3067
|
+
changed: true
|
|
3068
|
+
});
|
|
3069
|
+
return;
|
|
3070
|
+
}
|
|
3071
|
+
sequence += 1;
|
|
3072
|
+
sendNextOperation();
|
|
3073
|
+
}
|
|
2772
3074
|
});
|
|
2773
3075
|
});
|
|
2774
3076
|
}
|
|
@@ -2806,6 +3108,10 @@ var OnesWikiProductWriter = class {
|
|
|
2806
3108
|
const encodedTeamId = encodeIdentifier(teamId, "team UUID");
|
|
2807
3109
|
const headers = new Headers(init.headers);
|
|
2808
3110
|
headers.set("Authorization", `Bearer ${session.accessToken}`);
|
|
3111
|
+
headers.set("Ones-Auth-Token", session.legacyAuthToken);
|
|
3112
|
+
headers.set("Ones-User-Id", session.legacyUserId);
|
|
3113
|
+
headers.set("Referer", this.options.apiBase);
|
|
3114
|
+
if (session.cookieHeader) headers.set("Cookie", session.cookieHeader);
|
|
2809
3115
|
if (typeof init.body === "string" && !headers.has("Content-Type")) headers.set("Content-Type", "application/json;charset=UTF-8");
|
|
2810
3116
|
const response = await fetch(`${this.options.apiBase}/wiki/api/wiki/team/${encodedTeamId}${apiPath}`, {
|
|
2811
3117
|
...init,
|
|
@@ -2828,9 +3134,9 @@ var OnesWikiProductWriter = class {
|
|
|
2828
3134
|
}
|
|
2829
3135
|
throw new Error("ONES Wiki editor token endpoint was not found");
|
|
2830
3136
|
}
|
|
2831
|
-
async replaceDocument(teamId, resourceId, documentId, update, preferDraft = false) {
|
|
3137
|
+
async replaceDocument(teamId, resourceId, documentId, update, preferDraft = false, editorTokenOverride) {
|
|
2832
3138
|
const session = await this.options.getSession();
|
|
2833
|
-
const editorToken = await this.fetchEditorToken(teamId, resourceId, preferDraft);
|
|
3139
|
+
const editorToken = editorTokenOverride ?? await this.fetchEditorToken(teamId, resourceId, preferDraft);
|
|
2834
3140
|
return { version: (await replaceOnesWikiDocument({
|
|
2835
3141
|
baseUrl: this.options.apiBase,
|
|
2836
3142
|
teamId,
|
|
@@ -2838,6 +3144,7 @@ var OnesWikiProductWriter = class {
|
|
|
2838
3144
|
accessToken: session.accessToken,
|
|
2839
3145
|
editorToken,
|
|
2840
3146
|
userId: session.userUuid,
|
|
3147
|
+
cookieHeader: session.cookieHeader,
|
|
2841
3148
|
displayName: session.userName
|
|
2842
3149
|
}, update)).version };
|
|
2843
3150
|
}
|
|
@@ -2891,20 +3198,120 @@ var OnesWikiProductWriter = class {
|
|
|
2891
3198
|
};
|
|
2892
3199
|
}
|
|
2893
3200
|
async update(params) {
|
|
2894
|
-
const
|
|
2895
|
-
const
|
|
2896
|
-
|
|
2897
|
-
const
|
|
3201
|
+
const encodedPageId = encodeIdentifier(params.pageId, "Wiki page UUID");
|
|
3202
|
+
const encodedSpaceId = params.spaceId ? encodeIdentifier(params.spaceId, "Wiki space UUID") : null;
|
|
3203
|
+
let detail = await this.request(params.teamId, `/page/${encodedPageId}?action=edit`);
|
|
3204
|
+
const title = firstNonEmptyString(detail.title, detail.name, detail.page_title) ?? `Wiki ${params.pageId}`;
|
|
3205
|
+
if (detail.ref_type === 6 && detail.ref_uuid) {
|
|
3206
|
+
const editorToken = await this.fetchEditorToken(params.teamId, params.pageId);
|
|
3207
|
+
const write = await this.replaceDocument(params.teamId, params.pageId, detail.ref_uuid, (snapshot) => {
|
|
3208
|
+
return parseWikiDocument(applyWikiUpdateOperation(JSON.stringify(snapshot), params.operation));
|
|
3209
|
+
}, false, editorToken);
|
|
3210
|
+
const published = await this.request(params.teamId, `/online_page/${encodedPageId}/publish`, {
|
|
3211
|
+
method: "POST",
|
|
3212
|
+
body: JSON.stringify({ title })
|
|
3213
|
+
});
|
|
3214
|
+
if (params.spaceId) this.options.invalidateTree(params.teamId, params.spaceId);
|
|
3215
|
+
return {
|
|
3216
|
+
pageId: params.pageId,
|
|
3217
|
+
title,
|
|
3218
|
+
version: String(published.version ?? published.updated_time ?? write.version),
|
|
3219
|
+
url: params.spaceId ? `${this.options.apiBase}/wiki/#/team/${encodeURIComponent(params.teamId)}/space/${encodeURIComponent(params.spaceId)}/page/${encodeURIComponent(params.pageId)}` : null
|
|
3220
|
+
};
|
|
3221
|
+
}
|
|
3222
|
+
let draftId = firstNonEmptyString(detail.online_draft_uuid, detail.onlineDraftUuid, detail.online_draft?.uuid, detail.draft_uuid, detail.draftUuid, detail.draft?.uuid);
|
|
3223
|
+
let documentId = firstNonEmptyString(detail.online_draft_ref_uuid, detail.onlineDraftRefUuid, detail.online_draft?.ref_uuid, detail.draft_ref_uuid, detail.draftRefUuid, detail.draft?.ref_uuid, detail.ref_uuid);
|
|
3224
|
+
if (!draftId) {
|
|
3225
|
+
if (!encodedSpaceId) throw new Error("ONES Wiki page space is required to create an edit draft");
|
|
3226
|
+
try {
|
|
3227
|
+
const createdDraft = await this.request(params.teamId, `/space/${encodedSpaceId}/drafts/add`, {
|
|
3228
|
+
method: "POST",
|
|
3229
|
+
body: JSON.stringify({
|
|
3230
|
+
page_uuid: params.pageId,
|
|
3231
|
+
status: 2,
|
|
3232
|
+
title
|
|
3233
|
+
})
|
|
3234
|
+
});
|
|
3235
|
+
draftId = firstNonEmptyString(createdDraft.draft_uuid, createdDraft.draftUuid, createdDraft.uuid, createdDraft.id);
|
|
3236
|
+
documentId = firstNonEmptyString(createdDraft.ref_uuid, createdDraft.refUuid, documentId);
|
|
3237
|
+
} catch (error) {
|
|
3238
|
+
const refreshedDetail = await this.request(params.teamId, `/space/${encodedSpaceId}/page/${encodedPageId}`);
|
|
3239
|
+
draftId = firstNonEmptyString(refreshedDetail.draft_uuid, refreshedDetail.draftUuid);
|
|
3240
|
+
if (!draftId) throw error;
|
|
3241
|
+
detail = refreshedDetail;
|
|
3242
|
+
documentId = firstNonEmptyString(refreshedDetail.draft_ref_uuid, refreshedDetail.draftRefUuid, refreshedDetail.ref_uuid, documentId);
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
if (!draftId || !encodedSpaceId) throw new Error("ONES Wiki edit draft did not include the required resource and space IDs");
|
|
3246
|
+
const encodedDraftId = encodeIdentifier(draftId, "Wiki draft UUID");
|
|
3247
|
+
const draftDetail = await this.request(params.teamId, `/space/${encodedSpaceId}/draft/${encodedDraftId}`);
|
|
3248
|
+
documentId = firstNonEmptyString(draftDetail.ref_uuid, draftDetail.refUuid, documentId);
|
|
3249
|
+
if (!documentId) throw new Error("ONES Wiki edit draft did not include a collaborative document ID");
|
|
3250
|
+
if (typeof draftDetail.content === "string") {
|
|
3251
|
+
let content;
|
|
3252
|
+
if (params.operation.type === "replace_document") content = markdownToWikiHtml(params.operation.markdown);
|
|
3253
|
+
else if (params.operation.type === "append_blocks") content = `${draftDetail.content}\n${markdownToWikiHtml(params.operation.markdown)}`;
|
|
3254
|
+
else if (params.operation.type === "replace_text") {
|
|
3255
|
+
const occurrences = draftDetail.content.split(params.operation.find).length - 1;
|
|
3256
|
+
if (occurrences !== 1) throw new Error(`ONES Wiki draft replace_text requires exactly one match; found ${occurrences}`);
|
|
3257
|
+
content = draftDetail.content.replace(params.operation.find, params.operation.replace);
|
|
3258
|
+
} else throw new Error("ONES Wiki append_table_row is not supported for legacy page drafts");
|
|
3259
|
+
const published = await this.request(params.teamId, `/space/${encodedSpaceId}/draft/${encodedDraftId}/update`, {
|
|
3260
|
+
method: "POST",
|
|
3261
|
+
body: JSON.stringify({
|
|
3262
|
+
...draftDetail,
|
|
3263
|
+
content,
|
|
3264
|
+
title,
|
|
3265
|
+
page_uuid: params.pageId,
|
|
3266
|
+
space_uuid: params.spaceId,
|
|
3267
|
+
from_version: detail.version ?? draftDetail.from_version,
|
|
3268
|
+
is_published: true,
|
|
3269
|
+
is_forced: true
|
|
3270
|
+
})
|
|
3271
|
+
});
|
|
3272
|
+
this.options.invalidateTree(params.teamId, params.spaceId);
|
|
3273
|
+
const publishedVersion = published.version ?? published.updated_time ?? draftDetail.version ?? draftDetail.updated_time;
|
|
3274
|
+
return {
|
|
3275
|
+
pageId: params.pageId,
|
|
3276
|
+
title,
|
|
3277
|
+
version: publishedVersion === void 0 ? null : String(publishedVersion),
|
|
3278
|
+
url: `${this.options.apiBase}/wiki/#/team/${encodeURIComponent(params.teamId)}/space/${encodeURIComponent(params.spaceId)}/page/${encodeURIComponent(params.pageId)}`
|
|
3279
|
+
};
|
|
3280
|
+
}
|
|
3281
|
+
const write = await this.replaceDocument(params.teamId, draftId, documentId, (snapshot) => {
|
|
2898
3282
|
return parseWikiDocument(applyWikiUpdateOperation(JSON.stringify(snapshot), params.operation));
|
|
3283
|
+
}, true);
|
|
3284
|
+
const refreshedDraft = await this.request(params.teamId, `/space/${encodedSpaceId}/draft/${encodedDraftId}`);
|
|
3285
|
+
await this.request(params.teamId, `/space/${encodedSpaceId}/draft/${encodedDraftId}/update`, {
|
|
3286
|
+
method: "POST",
|
|
3287
|
+
body: JSON.stringify({
|
|
3288
|
+
...refreshedDraft,
|
|
3289
|
+
title,
|
|
3290
|
+
page_uuid: params.pageId,
|
|
3291
|
+
space_uuid: params.spaceId,
|
|
3292
|
+
from_version: detail.version ?? refreshedDraft.from_version,
|
|
3293
|
+
is_published: true,
|
|
3294
|
+
is_forced: true
|
|
3295
|
+
})
|
|
2899
3296
|
});
|
|
2900
3297
|
if (params.spaceId) this.options.invalidateTree(params.teamId, params.spaceId);
|
|
2901
3298
|
return {
|
|
2902
3299
|
pageId: params.pageId,
|
|
2903
|
-
title
|
|
3300
|
+
title,
|
|
2904
3301
|
version: String(write.version),
|
|
2905
3302
|
url: params.spaceId ? `${this.options.apiBase}/wiki/#/team/${encodeURIComponent(params.teamId)}/space/${encodeURIComponent(params.spaceId)}/page/${encodeURIComponent(params.pageId)}` : null
|
|
2906
3303
|
};
|
|
2907
3304
|
}
|
|
3305
|
+
async delete(params) {
|
|
3306
|
+
const encodedSpaceId = encodeIdentifier(params.spaceId, "Wiki space UUID");
|
|
3307
|
+
const encodedPageId = encodeIdentifier(params.pageId, "Wiki page UUID");
|
|
3308
|
+
await this.request(params.teamId, `/space/${encodedSpaceId}/page/${encodedPageId}/delete`, { method: "POST" });
|
|
3309
|
+
this.options.invalidateTree(params.teamId, params.spaceId);
|
|
3310
|
+
return {
|
|
3311
|
+
pageId: params.pageId,
|
|
3312
|
+
deleted: true
|
|
3313
|
+
};
|
|
3314
|
+
}
|
|
2908
3315
|
};
|
|
2909
3316
|
//#endregion
|
|
2910
3317
|
//#region ../../src/adapters/ones/task-query.ts
|
|
@@ -2991,7 +3398,6 @@ var OnesTaskAdapter = class extends BaseAdapter {
|
|
|
2991
3398
|
this.wikiProductWriter = new OnesWikiProductWriter({
|
|
2992
3399
|
apiBase: config.apiBase,
|
|
2993
3400
|
getSession: () => this.login(),
|
|
2994
|
-
fetchPageDetail: (pageId, teamId) => this.wikiReader.fetchPageDetail(pageId, teamId, true),
|
|
2995
3401
|
invalidateTree: (teamId, spaceId) => this.wikiReader.invalidateTree(teamId, spaceId)
|
|
2996
3402
|
});
|
|
2997
3403
|
this.content = new OnesTaskContent({
|
|
@@ -3007,7 +3413,10 @@ var OnesTaskAdapter = class extends BaseAdapter {
|
|
|
3007
3413
|
this.planning = new OnesTaskPlanning({
|
|
3008
3414
|
api: this.api,
|
|
3009
3415
|
getRequirement: (id) => this.getRequirement({ id }),
|
|
3010
|
-
fetchTaskInfo: (taskUuid) => this.fetchTaskInfo(taskUuid)
|
|
3416
|
+
fetchTaskInfo: (taskUuid) => this.fetchTaskInfo(taskUuid),
|
|
3417
|
+
resolveProjectIdentifier: async (projectUuid) => {
|
|
3418
|
+
return (await this.fetchProjects()).find((candidate) => candidate.uuid === projectUuid)?.identifier?.toUpperCase() ?? null;
|
|
3419
|
+
}
|
|
3011
3420
|
});
|
|
3012
3421
|
this.issueReader = new OnesIssueReader({
|
|
3013
3422
|
api: this.api,
|
|
@@ -3225,6 +3634,9 @@ var OnesTaskAdapter = class extends BaseAdapter {
|
|
|
3225
3634
|
async updateWikiPage(params) {
|
|
3226
3635
|
return this.wikiProductWriter.update(params);
|
|
3227
3636
|
}
|
|
3637
|
+
async deleteWikiPage(params) {
|
|
3638
|
+
return this.wikiProductWriter.delete(params);
|
|
3639
|
+
}
|
|
3228
3640
|
/**
|
|
3229
3641
|
* Fetch a work item by UUID, number, display id, or wiki URL.
|
|
3230
3642
|
* Routes by issueType.detailType: requirements (1 and 5) load wiki docs;
|
|
@@ -3619,7 +4031,7 @@ async function handleGetGrillingBrief(input, adapters, defaultSource) {
|
|
|
3619
4031
|
}
|
|
3620
4032
|
//#endregion
|
|
3621
4033
|
//#region ../../src/utils/safe-image.ts
|
|
3622
|
-
const DEFAULT_MAX_BYTES =
|
|
4034
|
+
const DEFAULT_MAX_BYTES = 8388608;
|
|
3623
4035
|
const DEFAULT_MAX_REDIRECTS = 3;
|
|
3624
4036
|
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
3625
4037
|
const MAX_IMAGES = 8;
|
|
@@ -3967,7 +4379,7 @@ function formatWorkItem(req) {
|
|
|
3967
4379
|
//#endregion
|
|
3968
4380
|
//#region ../../src/tools/list-pending-work-items.ts
|
|
3969
4381
|
const ListPendingWorkItemsSchema = z.object({ source: z.string().optional().describe("Source to read. If omitted, uses the default source.") });
|
|
3970
|
-
function resolveAdapter$
|
|
4382
|
+
function resolveAdapter$4(source, adapters, defaultSource) {
|
|
3971
4383
|
const sourceType = source ?? defaultSource;
|
|
3972
4384
|
if (!sourceType) throw new Error("No source specified and no default source configured");
|
|
3973
4385
|
const adapter = adapters.get(sourceType);
|
|
@@ -4011,7 +4423,7 @@ function formatResult(result) {
|
|
|
4011
4423
|
return lines.join("\n");
|
|
4012
4424
|
}
|
|
4013
4425
|
async function handleListPendingWorkItems(input, adapters, defaultSource) {
|
|
4014
|
-
const result = await resolveAdapter$
|
|
4426
|
+
const result = await resolveAdapter$4(input.source, adapters, defaultSource).listPendingWorkItems();
|
|
4015
4427
|
const safeResult = {
|
|
4016
4428
|
...result,
|
|
4017
4429
|
items: result.items.map(sanitizeItem)
|
|
@@ -4049,7 +4461,7 @@ async function handleListSources(adapters, config) {
|
|
|
4049
4461
|
}
|
|
4050
4462
|
//#endregion
|
|
4051
4463
|
//#region ../../src/tools/requirement-decomposition.ts
|
|
4052
|
-
const APPROVAL_TTL_MS$1 =
|
|
4464
|
+
const APPROVAL_TTL_MS$1 = 18e5;
|
|
4053
4465
|
const MAX_CREATE_OPERATIONS = 10;
|
|
4054
4466
|
function isValidDate(value) {
|
|
4055
4467
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
|
|
@@ -4125,7 +4537,7 @@ var RequirementDecompositionApprovalStore = class {
|
|
|
4125
4537
|
return record;
|
|
4126
4538
|
}
|
|
4127
4539
|
};
|
|
4128
|
-
function resolveAdapter$
|
|
4540
|
+
function resolveAdapter$3(source, adapters, defaultSource) {
|
|
4129
4541
|
const sourceType = source ?? defaultSource;
|
|
4130
4542
|
if (!sourceType) throw new Error("No source specified and no default source configured");
|
|
4131
4543
|
const adapter = adapters.get(sourceType);
|
|
@@ -4228,7 +4640,7 @@ function buildOperations(displayId, tasks) {
|
|
|
4228
4640
|
});
|
|
4229
4641
|
}
|
|
4230
4642
|
async function handleInspectRequirementDecomposition(input, adapters, defaultSource) {
|
|
4231
|
-
const { adapter } = resolveAdapter$
|
|
4643
|
+
const { adapter } = resolveAdapter$3(input.source, adapters, defaultSource);
|
|
4232
4644
|
const context = sanitizedContext(await adapter.getRequirementDecompositionContext({ requirementId: input.requirementId }));
|
|
4233
4645
|
return {
|
|
4234
4646
|
content: [{
|
|
@@ -4239,7 +4651,7 @@ async function handleInspectRequirementDecomposition(input, adapters, defaultSou
|
|
|
4239
4651
|
};
|
|
4240
4652
|
}
|
|
4241
4653
|
async function handlePrepareRequirementDecomposition(input, adapters, approvals, defaultSource) {
|
|
4242
|
-
const { sourceType, adapter } = resolveAdapter$
|
|
4654
|
+
const { sourceType, adapter } = resolveAdapter$3(input.source, adapters, defaultSource);
|
|
4243
4655
|
const context = await adapter.getRequirementDecompositionContext({ requirementId: input.requirementId });
|
|
4244
4656
|
if (context.requirement.workItemKind !== "requirement") throw new Error("Only requirements can be decomposed");
|
|
4245
4657
|
if (!context.decompositionRelation.verified || !context.decompositionRelation.uuid) throw new Error("The \"requirement decomposition task\" relationship could not be verified from the read response. No plan or write was prepared.");
|
|
@@ -4289,7 +4701,7 @@ async function handleApplyRequirementDecomposition(input, adapters, approvals, o
|
|
|
4289
4701
|
if (!record) throw new Error("Approval token is invalid, expired, or already used. Prepare the decomposition again.");
|
|
4290
4702
|
if ((input.source ?? options.defaultSource) !== record.source) throw new Error("Approval token source does not match the requested source");
|
|
4291
4703
|
if (input.planHash !== record.planHash) throw new Error("Plan hash does not match the approved decomposition");
|
|
4292
|
-
const { adapter } = resolveAdapter$
|
|
4704
|
+
const { adapter } = resolveAdapter$3(record.source, adapters, options.defaultSource);
|
|
4293
4705
|
const current = await adapter.getRequirementDecompositionContext({ requirementId: record.requirementId });
|
|
4294
4706
|
if (current.requirement.uuid !== record.requirementUuid || !isSameRequirementBaseline(current.baseline, record.baseline)) throw new Error("Requirement or related tasks changed after preparation. Prepare and confirm a new decomposition.");
|
|
4295
4707
|
if (!current.decompositionRelation.verified || current.decompositionRelation.uuid !== record.decompositionRelation.uuid) throw new Error("The requirement decomposition relationship changed or is no longer verified. Prepare and confirm again.");
|
|
@@ -4395,12 +4807,82 @@ function formatUpdateTaskPlanDatesResult(result) {
|
|
|
4395
4807
|
return lines.join("\n");
|
|
4396
4808
|
}
|
|
4397
4809
|
//#endregion
|
|
4810
|
+
//#region ../../src/tools/wiki-delete.ts
|
|
4811
|
+
const SourceSchema$1 = z.string().trim().min(1).optional();
|
|
4812
|
+
const DeleteEmptyWikiDuplicatesSchema = z.object({
|
|
4813
|
+
keepPageId: z.string().trim().min(1),
|
|
4814
|
+
duplicatePageIds: z.array(z.string().trim().min(1)).min(1).max(20),
|
|
4815
|
+
expectedTitle: z.string().trim().min(1),
|
|
4816
|
+
confirmed: z.literal(true).describe("Set true only after the user confirms the exact keep and delete page IDs immediately before submission."),
|
|
4817
|
+
source: SourceSchema$1
|
|
4818
|
+
});
|
|
4819
|
+
const DeleteEmptyWikiDuplicatesOutputSchema = z.object({
|
|
4820
|
+
keptPageId: z.string(),
|
|
4821
|
+
deletedPageIds: z.array(z.string())
|
|
4822
|
+
});
|
|
4823
|
+
function resolveAdapter$2(source, adapters, defaultSource) {
|
|
4824
|
+
const sourceType = source ?? defaultSource;
|
|
4825
|
+
if (!sourceType) throw new Error("No source specified and no default source configured");
|
|
4826
|
+
const adapter = adapters.get(sourceType);
|
|
4827
|
+
if (!adapter) throw new Error(`Source "${sourceType}" is not configured`);
|
|
4828
|
+
return {
|
|
4829
|
+
sourceType,
|
|
4830
|
+
adapter
|
|
4831
|
+
};
|
|
4832
|
+
}
|
|
4833
|
+
function assertCleanupTarget(keep, duplicates, expectedTitle) {
|
|
4834
|
+
if (keep.title !== expectedTitle) throw new Error("The retained Wiki page title no longer matches the confirmed title");
|
|
4835
|
+
if (keep.content.trim() === `# ${keep.title}`) throw new Error("The retained Wiki page does not contain a body");
|
|
4836
|
+
for (const duplicate of duplicates) {
|
|
4837
|
+
if (duplicate.title !== expectedTitle) throw new Error(`Wiki duplicate ${duplicate.pageId} title no longer matches the confirmed title`);
|
|
4838
|
+
if (duplicate.teamId !== keep.teamId || duplicate.spaceId !== keep.spaceId || duplicate.parentPageId !== keep.parentPageId) throw new Error(`Wiki duplicate ${duplicate.pageId} is not a sibling of the retained page`);
|
|
4839
|
+
if (duplicate.content.trim() !== `# ${duplicate.title}`) throw new Error(`Wiki duplicate ${duplicate.pageId} is not empty and will not be deleted`);
|
|
4840
|
+
}
|
|
4841
|
+
}
|
|
4842
|
+
async function handleDeleteEmptyWikiDuplicates(input, adapters, options) {
|
|
4843
|
+
if (!options.writesEnabled) throw new Error("Wiki writes are disabled. Enable both ONES_WIKI_ENABLE_WRITES=true and source option wikiWrites=true.");
|
|
4844
|
+
const uniqueDuplicateIds = [...new Set(input.duplicatePageIds)];
|
|
4845
|
+
if (uniqueDuplicateIds.length !== input.duplicatePageIds.length) throw new Error("duplicatePageIds contains repeated page IDs");
|
|
4846
|
+
if (uniqueDuplicateIds.includes(input.keepPageId)) throw new Error("The retained Wiki page cannot also be deleted");
|
|
4847
|
+
const { adapter } = resolveAdapter$2(input.source, adapters, options.defaultSource);
|
|
4848
|
+
const [keep, ...duplicates] = await Promise.all([adapter.getWikiPage({ pageId: input.keepPageId }), ...uniqueDuplicateIds.map((pageId) => adapter.getWikiPage({ pageId }))]);
|
|
4849
|
+
assertCleanupTarget(keep, duplicates, input.expectedTitle);
|
|
4850
|
+
const spaceId = keep.spaceId;
|
|
4851
|
+
if (!spaceId) throw new Error("The retained Wiki page space could not be verified");
|
|
4852
|
+
const operationHash = crypto.createHash("sha256").update(JSON.stringify({
|
|
4853
|
+
keepPageId: input.keepPageId,
|
|
4854
|
+
duplicatePageIds: uniqueDuplicateIds,
|
|
4855
|
+
expectedTitle: input.expectedTitle,
|
|
4856
|
+
baselines: [keep, ...duplicates].map((page) => ({
|
|
4857
|
+
pageId: page.pageId,
|
|
4858
|
+
version: page.version,
|
|
4859
|
+
contentHash: page.contentHash
|
|
4860
|
+
}))
|
|
4861
|
+
})).digest("hex");
|
|
4862
|
+
for (const duplicate of duplicates) await adapter.deleteWikiPage({
|
|
4863
|
+
teamId: duplicate.teamId,
|
|
4864
|
+
spaceId,
|
|
4865
|
+
pageId: duplicate.pageId
|
|
4866
|
+
});
|
|
4867
|
+
const result = {
|
|
4868
|
+
keptPageId: keep.pageId,
|
|
4869
|
+
deletedPageIds: duplicates.map((page) => page.pageId)
|
|
4870
|
+
};
|
|
4871
|
+
return {
|
|
4872
|
+
content: [{
|
|
4873
|
+
type: "text",
|
|
4874
|
+
text: `Kept Wiki page ${result.keptPageId} and deleted empty duplicates: ${result.deletedPageIds.join(", ")}.\noperationHash: ${operationHash}`
|
|
4875
|
+
}],
|
|
4876
|
+
structuredContent: result
|
|
4877
|
+
};
|
|
4878
|
+
}
|
|
4879
|
+
//#endregion
|
|
4398
4880
|
//#region ../../src/tools/wiki-read.ts
|
|
4399
4881
|
const WikiSourceSchema = z.string().trim().min(1).optional();
|
|
4400
4882
|
const GetOnesWikiPageSchema = z.object({
|
|
4401
4883
|
pageId: z.string().trim().min(1).optional(),
|
|
4402
4884
|
url: z.string().url().optional(),
|
|
4403
|
-
path: z.union([z.string().trim().min(1), z.array(z.string().trim().min(1)).min(1)]).optional().describe("Wiki path such as \"Department/Annual Plans/2026\". Resolves a unique exact, title-prefix, or confidently close match; otherwise returns candidate pages for confirmation."),
|
|
4885
|
+
path: z.union([z.string().trim().min(1), z.array(z.string().trim().min(1)).min(1)]).optional().describe("Wiki path such as \"Department/Annual Plans/2026\". Exact self-reference segments such as \"我的\", \"我\", \"me\", or \"my\" resolve the authenticated user through ONES token info without exposing the display name. Resolves a unique exact, title-prefix, or confidently close match; otherwise returns candidate pages for confirmation."),
|
|
4404
4886
|
teamId: z.string().trim().min(1).optional(),
|
|
4405
4887
|
spaceId: z.string().trim().min(1).optional(),
|
|
4406
4888
|
revealSensitiveSecrets: z.boolean().default(false).describe("Default false. Set true only when the user explicitly asks to reveal secrets."),
|
|
@@ -4502,6 +4984,19 @@ async function handleGetOnesWikiPage(input, adapters, defaultSource) {
|
|
|
4502
4984
|
teamId: resolved?.teamId ?? input.teamId,
|
|
4503
4985
|
spaceId: resolved?.spaceId ?? input.spaceId
|
|
4504
4986
|
}), input.revealSensitiveSecrets);
|
|
4987
|
+
if (resolved) {
|
|
4988
|
+
page.title = resolved.title;
|
|
4989
|
+
page.breadcrumb = resolved.breadcrumb;
|
|
4990
|
+
const redactPrivateValues = resolved.redactPrivateValues;
|
|
4991
|
+
if (redactPrivateValues) {
|
|
4992
|
+
page.content = redactPrivateValues(page.content);
|
|
4993
|
+
page.attachments = page.attachments.map((attachment) => ({
|
|
4994
|
+
...attachment,
|
|
4995
|
+
name: redactPrivateValues(attachment.name),
|
|
4996
|
+
url: redactPrivateValues(attachment.url)
|
|
4997
|
+
}));
|
|
4998
|
+
}
|
|
4999
|
+
}
|
|
4505
5000
|
return {
|
|
4506
5001
|
content: [{
|
|
4507
5002
|
type: "text",
|
|
@@ -4647,9 +5142,9 @@ async function handleLookupEnvironmentAccess(input, adapters, defaultSource) {
|
|
|
4647
5142
|
}
|
|
4648
5143
|
//#endregion
|
|
4649
5144
|
//#region ../../src/tools/wiki-write.ts
|
|
4650
|
-
const APPROVAL_TTL_MS =
|
|
5145
|
+
const APPROVAL_TTL_MS = 18e5;
|
|
4651
5146
|
const SourceSchema = z.string().trim().min(1).optional();
|
|
4652
|
-
const PathSchema = z.union([z.string().trim().min(1), z.array(z.string().trim().min(1)).min(1)]);
|
|
5147
|
+
const PathSchema = z.union([z.string().trim().min(1), z.array(z.string().trim().min(1)).min(1)]).describe("Wiki path. Exact self-reference segments such as \"我的\", \"我\", \"me\", or \"my\" resolve the authenticated user through ONES token info without exposing the display name.");
|
|
4653
5148
|
const PrepareWikiCreateSchema = z.object({
|
|
4654
5149
|
parentPageId: z.string().trim().min(1).optional(),
|
|
4655
5150
|
parentPath: PathSchema.optional(),
|
|
@@ -4673,6 +5168,10 @@ const ReplaceTextSchema = z.object({
|
|
|
4673
5168
|
find: z.string().min(1),
|
|
4674
5169
|
replace: z.string()
|
|
4675
5170
|
});
|
|
5171
|
+
const ReplaceDocumentSchema = z.object({
|
|
5172
|
+
type: z.literal("replace_document"),
|
|
5173
|
+
markdown: z.string().trim().min(1)
|
|
5174
|
+
});
|
|
4676
5175
|
const PrepareWikiUpdateSchema = z.object({
|
|
4677
5176
|
pageId: z.string().trim().min(1).optional(),
|
|
4678
5177
|
url: z.string().url().optional(),
|
|
@@ -4682,7 +5181,8 @@ const PrepareWikiUpdateSchema = z.object({
|
|
|
4682
5181
|
operation: z.discriminatedUnion("type", [
|
|
4683
5182
|
AppendBlocksSchema,
|
|
4684
5183
|
AppendTableRowSchema,
|
|
4685
|
-
ReplaceTextSchema
|
|
5184
|
+
ReplaceTextSchema,
|
|
5185
|
+
ReplaceDocumentSchema
|
|
4686
5186
|
]),
|
|
4687
5187
|
source: SourceSchema
|
|
4688
5188
|
}).refine((value) => [
|
|
@@ -4723,6 +5223,10 @@ const WikiUpdateOperationSchema = z.discriminatedUnion("type", [
|
|
|
4723
5223
|
type: z.literal("replace_text"),
|
|
4724
5224
|
find: z.string(),
|
|
4725
5225
|
replace: z.string()
|
|
5226
|
+
}),
|
|
5227
|
+
z.object({
|
|
5228
|
+
type: z.literal("replace_document"),
|
|
5229
|
+
markdown: z.string()
|
|
4726
5230
|
})
|
|
4727
5231
|
]);
|
|
4728
5232
|
const WikiUpdateRequestSchema = z.object({
|
|
@@ -4852,11 +5356,24 @@ async function handlePrepareWikiCreate(input, adapters, approvals, defaultSource
|
|
|
4852
5356
|
const teamId = resolved?.teamId ?? parent.teamId;
|
|
4853
5357
|
const spaceId = resolved?.spaceId ?? parent.spaceId;
|
|
4854
5358
|
if (!spaceId) throw new Error("The target space could not be verified");
|
|
5359
|
+
const title = input.title.trim();
|
|
5360
|
+
const titleCandidates = await adapter.searchWikiPages({
|
|
5361
|
+
query: title,
|
|
5362
|
+
teamId,
|
|
5363
|
+
spaceId,
|
|
5364
|
+
limit: 50
|
|
5365
|
+
});
|
|
5366
|
+
const siblingConflicts = (await Promise.all(titleCandidates.filter((candidate) => candidate.title === title).map((candidate) => adapter.getWikiPage({
|
|
5367
|
+
pageId: candidate.pageId,
|
|
5368
|
+
teamId: candidate.teamId,
|
|
5369
|
+
spaceId: candidate.spaceId ?? spaceId
|
|
5370
|
+
})))).filter((page) => page.parentPageId === parent.pageId);
|
|
5371
|
+
if (siblingConflicts.length) throw new Error(`Wiki title already exists under the selected parent (${siblingConflicts.map((page) => page.pageId).join(", ")}). Ask the user to choose one action: edit the existing page, delete it and recreate, or create with a new title.`);
|
|
4855
5372
|
const requestWithoutKey = {
|
|
4856
5373
|
teamId,
|
|
4857
5374
|
spaceId,
|
|
4858
5375
|
parentPageId: parent.pageId,
|
|
4859
|
-
title
|
|
5376
|
+
title,
|
|
4860
5377
|
markdown: input.markdown
|
|
4861
5378
|
};
|
|
4862
5379
|
const operationHash = hashOperation({
|
|
@@ -4878,7 +5395,7 @@ async function handlePrepareWikiCreate(input, adapters, approvals, defaultSource
|
|
|
4878
5395
|
});
|
|
4879
5396
|
const plan = {
|
|
4880
5397
|
kind: "create",
|
|
4881
|
-
targetBreadcrumb: [...parent.breadcrumb, input.title.trim()],
|
|
5398
|
+
targetBreadcrumb: [...resolved?.breadcrumb ?? parent.breadcrumb, input.title.trim()],
|
|
4882
5399
|
request,
|
|
4883
5400
|
parentBaseline: baseline(parent),
|
|
4884
5401
|
operationHash,
|
|
@@ -4935,7 +5452,7 @@ async function handlePrepareWikiUpdate(input, adapters, approvals, defaultSource
|
|
|
4935
5452
|
});
|
|
4936
5453
|
const plan = {
|
|
4937
5454
|
kind: "update",
|
|
4938
|
-
targetBreadcrumb: page.breadcrumb,
|
|
5455
|
+
targetBreadcrumb: resolved?.breadcrumb ?? page.breadcrumb,
|
|
4939
5456
|
request,
|
|
4940
5457
|
operationHash,
|
|
4941
5458
|
approvalToken: approval.token,
|
|
@@ -4945,7 +5462,7 @@ async function handlePrepareWikiUpdate(input, adapters, approvals, defaultSource
|
|
|
4945
5462
|
content: [{
|
|
4946
5463
|
type: "text",
|
|
4947
5464
|
text: [
|
|
4948
|
-
`Prepared Wiki update for ${
|
|
5465
|
+
`Prepared Wiki update for ${plan.targetBreadcrumb.join(" / ") || page.title}.`,
|
|
4949
5466
|
"No write was performed. Ask the user to confirm this exact operation immediately before apply.",
|
|
4950
5467
|
`operationHash: ${plan.operationHash}`,
|
|
4951
5468
|
`approvalToken: ${plan.approvalToken}`,
|
|
@@ -5058,7 +5575,7 @@ function createRequirementsServer(config, adapterOverrides) {
|
|
|
5058
5575
|
});
|
|
5059
5576
|
server.registerTool("get_ones_wiki_page", {
|
|
5060
5577
|
title: "Get ONES Wiki Page",
|
|
5061
|
-
description: "Read one ONES Wiki page as Markdown by page ID, URL, or hierarchical path. Prefer path for requests like \"Department/Annual Plans/2026\". Unique confidently close paths are corrected automatically; unresolved or ambiguous paths return candidate pages for confirmation. Sensitive values are redacted by default; revealing them requires an explicit argument based on an explicit user request.",
|
|
5578
|
+
description: "Read one ONES Wiki page as Markdown by page ID, URL, or hierarchical path. Prefer path for requests like \"Department/Annual Plans/2026\". Exact self-reference path segments such as \"我的\", \"我\", \"me\", or \"my\" resolve the authenticated user through ONES token info; never infer or expose the display name from local Git or machine configuration. Unique confidently close paths are corrected automatically; unresolved or ambiguous paths return candidate pages for confirmation. Sensitive values are redacted by default; revealing them requires an explicit argument based on an explicit user request.",
|
|
5062
5579
|
inputSchema: GetOnesWikiPageSchema,
|
|
5063
5580
|
annotations: {
|
|
5064
5581
|
readOnlyHint: true,
|
|
@@ -5120,7 +5637,7 @@ function createRequirementsServer(config, adapterOverrides) {
|
|
|
5120
5637
|
});
|
|
5121
5638
|
server.registerTool("prepare_wiki_create", {
|
|
5122
5639
|
title: "Prepare ONES Wiki Create",
|
|
5123
|
-
description: "Resolve an exact parent page and prepare one exact Wiki create operation. Never writes. Returns a one-time 30-minute approval token.",
|
|
5640
|
+
description: "Resolve an exact parent page and prepare one exact Wiki create operation. If a sibling page already has the requested title, fail closed and require the user to choose: edit the existing page, delete it and recreate, or use a new title. Exact self-reference path segments such as \"我的\", \"我\", \"me\", or \"my\" resolve the authenticated user through ONES token info; never infer or expose the display name from local Git or machine configuration. Never writes. Returns a one-time 30-minute approval token.",
|
|
5124
5641
|
inputSchema: PrepareWikiCreateSchema,
|
|
5125
5642
|
outputSchema: PrepareWikiCreateOutputSchema,
|
|
5126
5643
|
annotations: {
|
|
@@ -5159,7 +5676,7 @@ function createRequirementsServer(config, adapterOverrides) {
|
|
|
5159
5676
|
});
|
|
5160
5677
|
server.registerTool("prepare_wiki_update", {
|
|
5161
5678
|
title: "Prepare ONES Wiki Update",
|
|
5162
|
-
description: "Prepare an exact minimal Wiki update, including exact table-row targeting. Never writes. Ambiguous pages, tables, or text matches fail closed.",
|
|
5679
|
+
description: "Prepare an exact minimal Wiki update, including exact table-row targeting. Exact self-reference path segments such as \"我的\", \"我\", \"me\", or \"my\" resolve the authenticated user through ONES token info; never infer or expose the display name from local Git or machine configuration. Never writes. Ambiguous pages, tables, or text matches fail closed.",
|
|
5163
5680
|
inputSchema: PrepareWikiUpdateSchema,
|
|
5164
5681
|
outputSchema: PrepareWikiUpdateOutputSchema,
|
|
5165
5682
|
annotations: {
|
|
@@ -5196,6 +5713,28 @@ function createRequirementsServer(config, adapterOverrides) {
|
|
|
5196
5713
|
return toolError(err);
|
|
5197
5714
|
}
|
|
5198
5715
|
});
|
|
5716
|
+
server.registerTool("delete_empty_wiki_duplicates", {
|
|
5717
|
+
title: "Delete Empty ONES Wiki Duplicates",
|
|
5718
|
+
description: "Delete only explicitly confirmed, title-only duplicate sibling pages while retaining one verified page with a body. Revalidates every page immediately before deletion and fails closed if any duplicate contains additional content.",
|
|
5719
|
+
inputSchema: DeleteEmptyWikiDuplicatesSchema,
|
|
5720
|
+
outputSchema: DeleteEmptyWikiDuplicatesOutputSchema,
|
|
5721
|
+
annotations: {
|
|
5722
|
+
readOnlyHint: false,
|
|
5723
|
+
destructiveHint: true,
|
|
5724
|
+
idempotentHint: false,
|
|
5725
|
+
openWorldHint: true
|
|
5726
|
+
}
|
|
5727
|
+
}, async (params) => {
|
|
5728
|
+
try {
|
|
5729
|
+
const sourceType = params.source ?? defaultSource;
|
|
5730
|
+
return await handleDeleteEmptyWikiDuplicates(params, adapters, {
|
|
5731
|
+
defaultSource,
|
|
5732
|
+
writesEnabled: wikiWritesEnabled(sourceType)
|
|
5733
|
+
});
|
|
5734
|
+
} catch (err) {
|
|
5735
|
+
return toolError(err);
|
|
5736
|
+
}
|
|
5737
|
+
});
|
|
5199
5738
|
server.registerTool("list_sources", {
|
|
5200
5739
|
title: "List Sources",
|
|
5201
5740
|
description: "List all configured requirement sources and their status",
|