@aigne/doc-smith 0.8.12-beta.1 → 0.8.12-beta.3

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.
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.8.12-beta.1"
2
+ ".": "0.8.12-beta.3"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.12-beta.3](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.8.12-beta.2...v0.8.12-beta.3) (2025-10-09)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * should not require admin when publish to cloud ([0a74dd1](https://github.com/AIGNE-io/aigne-doc-smith/commit/0a74dd19a2c2390ca223d0cc393661bf3f408a6f))
9
+
10
+ ## [0.8.12-beta.2](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.8.12-beta.1...v0.8.12-beta.2) (2025-10-09)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * fix update document include path ([#166](https://github.com/AIGNE-io/aigne-doc-smith/issues/166)) ([40c3aa2](https://github.com/AIGNE-io/aigne-doc-smith/commit/40c3aa2739cb35d36fb1daed87a0e23c81285328))
16
+ * resolve failure in document update and generate ([#168](https://github.com/AIGNE-io/aigne-doc-smith/issues/168)) ([c00c759](https://github.com/AIGNE-io/aigne-doc-smith/commit/c00c759473a01ce3d16b89f2bfa974f43420b82a))
17
+
3
18
  ## [0.8.12-beta.1](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.8.12-beta...v0.8.12-beta.1) (2025-10-08)
4
19
 
5
20
 
@@ -6,7 +6,7 @@ import fs from "fs-extra";
6
6
 
7
7
  import { getAccessToken } from "../../utils/auth-utils.mjs";
8
8
  import {
9
- DEFAULT_APP_URL,
9
+ CLOUD_SERVICE_URL_PROD,
10
10
  DISCUSS_KIT_STORE_URL,
11
11
  DOC_SMITH_DIR,
12
12
  TMP_DIR,
@@ -45,7 +45,7 @@ export default async function publishDocs(
45
45
 
46
46
  // Check if appUrl is default and not saved in config (only when not using env variable)
47
47
  const config = await loadConfigFromFile();
48
- const isDefaultAppUrl = appUrl === DEFAULT_APP_URL;
48
+ const isDefaultAppUrl = appUrl === CLOUD_SERVICE_URL_PROD;
49
49
  const hasAppUrlInConfig = config?.appUrl;
50
50
 
51
51
  let token = "";
@@ -207,7 +207,7 @@ publishDocs.input_schema = {
207
207
  appUrl: {
208
208
  type: "string",
209
209
  description: "The url of the app",
210
- default: DEFAULT_APP_URL,
210
+ default: CLOUD_SERVICE_URL_PROD,
211
211
  },
212
212
  boardId: {
213
213
  type: "string",
@@ -112,7 +112,11 @@ export default async function checkDocument(
112
112
 
113
113
  const teamAgent = TeamAgent.from({
114
114
  name: "generateDocument",
115
- skills: [options.context.agents["handleDocumentUpdate"]],
115
+ skills: [
116
+ options.context.agents["handleDocumentUpdate"],
117
+ options.context.agents["translateMultilingual"],
118
+ options.context.agents["saveSingleDoc"],
119
+ ],
116
120
  });
117
121
 
118
122
  const result = await options.context.invoke(teamAgent, {
@@ -3,6 +3,10 @@ import { recordUpdate } from "../../utils/history-utils.mjs";
3
3
  export default async function saveAndTranslateDocument(input, options) {
4
4
  const { selectedDocs, docsDir, translateLanguages, locale } = input;
5
5
 
6
+ if (!Array.isArray(selectedDocs) || selectedDocs.length === 0) {
7
+ return {};
8
+ }
9
+
6
10
  // Saves a document with optional translation data
7
11
  const saveDocument = async (doc, translates = null, isTranslate = false) => {
8
12
  const saveAgent = options.context.agents["saveSingleDoc"];
@@ -40,7 +44,7 @@ export default async function saveAndTranslateDocument(input, options) {
40
44
  shouldTranslate = choice === "yes";
41
45
  }
42
46
 
43
- // Process documents in batches for better performance
47
+ // Save documents in batches
44
48
  const batchSize = 3;
45
49
  for (let i = 0; i < selectedDocs.length; i += batchSize) {
46
50
  const batch = selectedDocs.slice(i, i + batchSize);
@@ -56,8 +60,6 @@ export default async function saveAndTranslateDocument(input, options) {
56
60
  feedback: doc.feedback.trim(),
57
61
  documentPath: doc.path,
58
62
  });
59
- // clear feedback
60
- doc.feedback = "";
61
63
  }
62
64
  } catch (error) {
63
65
  console.error(`❌ Failed to save document ${doc.path}:`, error.message);
@@ -80,8 +82,11 @@ export default async function saveAndTranslateDocument(input, options) {
80
82
 
81
83
  const translatePromises = batch.map(async (doc) => {
82
84
  try {
85
+ // Clear feedback to ensure translation is not affected by update feedback
86
+ doc.feedback = "";
87
+
83
88
  const result = await options.context.invoke(translateAgent, {
84
- ...input, // Pass context for translation
89
+ ...input, // context is required
85
90
  content: doc.content,
86
91
  translates: doc.translates,
87
92
  title: doc.title,
@@ -245,7 +245,13 @@ export default async function userReviewDocument(
245
245
  }
246
246
  }
247
247
 
248
- return { content: options.context.userContext.currentContent, feedback: feedbacks.join(". ") };
248
+ return {
249
+ title,
250
+ description,
251
+ ...rest,
252
+ content: options.context.userContext.currentContent,
253
+ feedback: feedbacks.join(". "),
254
+ };
249
255
  }
250
256
 
251
257
  userReviewDocument.taskTitle = "User review and modify document content";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/doc-smith",
3
- "version": "0.8.12-beta.1",
3
+ "version": "0.8.12-beta.3",
4
4
  "description": "AI-driven documentation generation tool built on the AIGNE Framework",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -9,7 +9,7 @@
9
9
  </user_rules>
10
10
 
11
11
  {% set operation_type = "optimizing" %}
12
- {% include "../common/document/user-preferences.md" %}
12
+ {% include "../../common/document/user-preferences.md" %}
13
13
 
14
14
  <original_page_content>
15
15
  {{originalContent}}
@@ -25,7 +25,7 @@
25
25
  {{ assetsContent }}
26
26
  </media_list>
27
27
 
28
- {% include "../common/document/media-handling-rules.md" %}
28
+ {% include "../../common/document/media-handling-rules.md" %}
29
29
  </datasources>
30
30
 
31
31
  <user_feedback>
@@ -0,0 +1,369 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test";
2
+ import saveAndTranslateDocument from "../../../agents/update/save-and-translate-document.mjs";
3
+ import * as historyUtils from "../../../utils/history-utils.mjs";
4
+
5
+ describe("save-and-translate-document", () => {
6
+ let mockOptions;
7
+ let consoleErrorSpy;
8
+ let recordUpdateSpy;
9
+
10
+ beforeEach(() => {
11
+ // Reset all mocks
12
+ mock.restore();
13
+
14
+ mockOptions = {
15
+ prompts: {
16
+ select: mock(async () => "no"),
17
+ },
18
+ context: {
19
+ agents: {
20
+ saveSingleDoc: { mockSaveAgent: true },
21
+ translateMultilingual: { mockTranslateAgent: true },
22
+ },
23
+ invoke: mock(async () => ({ mockResult: true })),
24
+ },
25
+ };
26
+
27
+ consoleErrorSpy = spyOn(console, "error").mockImplementation(() => {});
28
+ recordUpdateSpy = spyOn(historyUtils, "recordUpdate").mockImplementation(() => {});
29
+
30
+ // Clear context mock call history
31
+ mockOptions.prompts.select.mockClear();
32
+ mockOptions.context.invoke.mockClear();
33
+ });
34
+
35
+ afterEach(() => {
36
+ consoleErrorSpy?.mockRestore();
37
+ recordUpdateSpy?.mockRestore();
38
+ });
39
+
40
+ // INPUT VALIDATION TESTS
41
+ test("should handle empty or invalid selectedDocs", async () => {
42
+ const testCases = [
43
+ { selectedDocs: [], description: "empty array" },
44
+ { selectedDocs: null, description: "null" },
45
+ { selectedDocs: undefined, description: "undefined" },
46
+ { selectedDocs: "not-array", description: "non-array" },
47
+ ];
48
+
49
+ for (const testCase of testCases) {
50
+ const input = {
51
+ selectedDocs: testCase.selectedDocs,
52
+ docsDir: "./docs",
53
+ translateLanguages: ["en", "zh"],
54
+ locale: "en",
55
+ };
56
+
57
+ const result = await saveAndTranslateDocument(input, mockOptions);
58
+
59
+ expect(result).toEqual({});
60
+ expect(mockOptions.context.invoke).not.toHaveBeenCalled();
61
+ expect(mockOptions.prompts.select).not.toHaveBeenCalled();
62
+ }
63
+ });
64
+
65
+ // SCENARIO 1: NO TRANSLATION CONFIGURATION
66
+ test("should skip translation when no translation languages configured", async () => {
67
+ const testCases = [
68
+ { translateLanguages: null, description: "null" },
69
+ { translateLanguages: undefined, description: "undefined" },
70
+ { translateLanguages: [], description: "empty array" },
71
+ { translateLanguages: ["en"], description: "only current locale" },
72
+ ];
73
+
74
+ for (const testCase of testCases) {
75
+ const input = {
76
+ selectedDocs: [
77
+ {
78
+ path: "/docs/test.md",
79
+ content: "# Test Document",
80
+ translates: {},
81
+ labels: {},
82
+ feedback: "Good content",
83
+ },
84
+ ],
85
+ docsDir: "./docs",
86
+ translateLanguages: testCase.translateLanguages,
87
+ locale: "en",
88
+ };
89
+
90
+ const result = await saveAndTranslateDocument(input, mockOptions);
91
+
92
+ expect(result).toEqual({});
93
+ expect(mockOptions.prompts.select).not.toHaveBeenCalled();
94
+ expect(mockOptions.context.invoke).toHaveBeenCalledTimes(1);
95
+ expect(recordUpdateSpy).toHaveBeenCalledWith({
96
+ operation: "document_update",
97
+ feedback: "Good content",
98
+ documentPath: "/docs/test.md",
99
+ });
100
+
101
+ // Reset mocks for next iteration
102
+ mockOptions.prompts.select.mockClear();
103
+ mockOptions.context.invoke.mockClear();
104
+ recordUpdateSpy.mockClear();
105
+ }
106
+ });
107
+
108
+ // SCENARIO 2: USER CHOOSES NOT TO TRANSLATE
109
+ test("should save documents and skip translation when user chooses no", async () => {
110
+ const input = {
111
+ selectedDocs: [
112
+ {
113
+ path: "/docs/test1.md",
114
+ content: "# Test Document 1",
115
+ translates: {},
116
+ labels: {},
117
+ feedback: "Update needed",
118
+ },
119
+ {
120
+ path: "/docs/test2.md",
121
+ content: "# Test Document 2",
122
+ translates: {},
123
+ labels: {},
124
+ feedback: "Second feedback",
125
+ },
126
+ {
127
+ path: "/docs/test3.md",
128
+ content: "# Test Document 3",
129
+ translates: {},
130
+ labels: {},
131
+ feedback: " ", // Whitespace only
132
+ },
133
+ ],
134
+ docsDir: "./docs",
135
+ translateLanguages: ["en", "zh", "ja"],
136
+ locale: "en",
137
+ };
138
+
139
+ mockOptions.prompts.select.mockResolvedValue("no");
140
+
141
+ const result = await saveAndTranslateDocument(input, mockOptions);
142
+
143
+ expect(result).toEqual({});
144
+ expect(mockOptions.prompts.select).toHaveBeenCalledWith({
145
+ message: "Document update completed. Would you like to translate these documents now?",
146
+ choices: [
147
+ {
148
+ name: "Review documents first, translate later",
149
+ value: "no",
150
+ },
151
+ {
152
+ name: "Translate now",
153
+ value: "yes",
154
+ },
155
+ ],
156
+ });
157
+ expect(mockOptions.context.invoke).toHaveBeenCalledTimes(3); // Only saveDocument calls
158
+ expect(recordUpdateSpy).toHaveBeenCalledTimes(2); // Only documents with non-empty feedback
159
+ expect(recordUpdateSpy).toHaveBeenCalledWith({
160
+ operation: "document_update",
161
+ feedback: "Update needed",
162
+ documentPath: "/docs/test1.md",
163
+ });
164
+ expect(recordUpdateSpy).toHaveBeenCalledWith({
165
+ operation: "document_update",
166
+ feedback: "Second feedback",
167
+ documentPath: "/docs/test2.md",
168
+ });
169
+ });
170
+
171
+ // SCENARIO 3: USER CHOOSES TO TRANSLATE
172
+ test("should save and translate documents when user chooses yes", async () => {
173
+ const input = {
174
+ selectedDocs: [
175
+ {
176
+ path: "/docs/test1.md",
177
+ content: "# Test Document 1",
178
+ translates: {},
179
+ labels: {},
180
+ feedback: "Translation needed",
181
+ title: "Test Document 1",
182
+ },
183
+ {
184
+ path: "/docs/test2.md",
185
+ content: "# Test Document 2",
186
+ translates: {},
187
+ labels: {},
188
+ title: "Test Document 2",
189
+ },
190
+ ],
191
+ docsDir: "./docs",
192
+ translateLanguages: ["en", "zh"],
193
+ locale: "en",
194
+ };
195
+
196
+ mockOptions.prompts.select.mockResolvedValue("yes");
197
+ mockOptions.context.invoke
198
+ .mockResolvedValueOnce({ mockSaveResult: true }) // saveDocument 1
199
+ .mockResolvedValueOnce({ mockSaveResult: true }) // saveDocument 2
200
+ .mockResolvedValueOnce({ translates: { zh: "# 测试文档 1" } }) // translateMultilingual 1
201
+ .mockResolvedValueOnce({ translates: { zh: "# 测试文档 2" } }) // translateMultilingual 2
202
+ .mockResolvedValueOnce({ mockSaveResult: true }) // saveDocument with translation 1
203
+ .mockResolvedValueOnce({ mockSaveResult: true }); // saveDocument with translation 2
204
+
205
+ const result = await saveAndTranslateDocument(input, mockOptions);
206
+
207
+ expect(result).toEqual({});
208
+ expect(mockOptions.prompts.select).toHaveBeenCalledWith({
209
+ message: "Document update completed. Would you like to translate these documents now?",
210
+ choices: [
211
+ {
212
+ name: "Review documents first, translate later",
213
+ value: "no",
214
+ },
215
+ {
216
+ name: "Translate now",
217
+ value: "yes",
218
+ },
219
+ ],
220
+ });
221
+ expect(mockOptions.context.invoke).toHaveBeenCalledTimes(6);
222
+
223
+ // Verify feedback is cleared before translation
224
+ expect(input.selectedDocs[0].feedback).toBe("");
225
+ expect(input.selectedDocs[1].feedback).toBe("");
226
+
227
+ expect(recordUpdateSpy).toHaveBeenCalledWith({
228
+ operation: "document_update",
229
+ feedback: "Translation needed",
230
+ documentPath: "/docs/test1.md",
231
+ });
232
+ });
233
+
234
+ // ERROR HANDLING TESTS
235
+ test("should handle errors gracefully", async () => {
236
+ // Test saveDocument error
237
+ const saveErrorInput = {
238
+ selectedDocs: [
239
+ {
240
+ path: "/docs/test1.md",
241
+ content: "# Test Document 1",
242
+ translates: {},
243
+ labels: {},
244
+ feedback: "Error test",
245
+ },
246
+ ],
247
+ docsDir: "./docs",
248
+ translateLanguages: ["en", "zh"],
249
+ locale: "en",
250
+ };
251
+
252
+ mockOptions.prompts.select.mockResolvedValue("no");
253
+ mockOptions.context.invoke.mockRejectedValue(new Error("Save failed"));
254
+
255
+ const saveErrorResult = await saveAndTranslateDocument(saveErrorInput, mockOptions);
256
+
257
+ expect(saveErrorResult).toEqual({});
258
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
259
+ "❌ Failed to save document /docs/test1.md:",
260
+ "Save failed",
261
+ );
262
+ expect(recordUpdateSpy).not.toHaveBeenCalled(); // Should not record if save failed
263
+
264
+ // Reset mocks
265
+ mockOptions.prompts.select.mockClear();
266
+ mockOptions.context.invoke.mockClear();
267
+ consoleErrorSpy.mockClear();
268
+ recordUpdateSpy.mockClear();
269
+
270
+ // Test translateMultilingual error
271
+ const translateErrorInput = {
272
+ selectedDocs: [
273
+ {
274
+ path: "/docs/test2.md",
275
+ content: "# Test Document 2",
276
+ translates: {},
277
+ labels: {},
278
+ title: "Test Document 2",
279
+ },
280
+ ],
281
+ docsDir: "./docs",
282
+ translateLanguages: ["en", "zh"],
283
+ locale: "en",
284
+ };
285
+
286
+ mockOptions.prompts.select.mockResolvedValue("yes");
287
+ mockOptions.context.invoke
288
+ .mockResolvedValueOnce({ mockSaveResult: true }) // saveDocument succeeds
289
+ .mockRejectedValueOnce(new Error("Translation failed")); // translateMultilingual fails
290
+
291
+ const translateErrorResult = await saveAndTranslateDocument(translateErrorInput, mockOptions);
292
+
293
+ expect(translateErrorResult).toEqual({});
294
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
295
+ "❌ Failed to translate document /docs/test2.md:",
296
+ "Translation failed",
297
+ );
298
+ expect(mockOptions.context.invoke).toHaveBeenCalledTimes(2);
299
+ });
300
+
301
+ // EDGE CASES AND INTEGRATION
302
+ test("should handle edge cases and complex scenarios", async () => {
303
+ // Test edge cases with different document properties
304
+ const edgeCaseInput = {
305
+ selectedDocs: [
306
+ {
307
+ path: "/docs/test1.md",
308
+ content: "# Test Document 1",
309
+ translates: null, // null translates
310
+ labels: {},
311
+ feedback: "", // empty feedback
312
+ },
313
+ {
314
+ path: "/docs/test2.md",
315
+ content: "# Test Document 2",
316
+ translates: undefined, // undefined translates
317
+ labels: {},
318
+ feedback: " ", // whitespace feedback
319
+ },
320
+ {
321
+ path: "/docs/test3.md",
322
+ content: "# Test Document 3",
323
+ translates: {},
324
+ labels: {},
325
+ // no title
326
+ },
327
+ ],
328
+ docsDir: "./docs",
329
+ translateLanguages: ["en", "zh"],
330
+ locale: "en",
331
+ };
332
+
333
+ mockOptions.prompts.select.mockResolvedValue("no");
334
+
335
+ const edgeCaseResult = await saveAndTranslateDocument(edgeCaseInput, mockOptions);
336
+
337
+ expect(edgeCaseResult).toEqual({});
338
+ expect(mockOptions.context.invoke).toHaveBeenCalledTimes(3);
339
+ expect(recordUpdateSpy).not.toHaveBeenCalled(); // No valid feedback
340
+
341
+ // Reset mocks
342
+ mockOptions.prompts.select.mockClear();
343
+ mockOptions.context.invoke.mockClear();
344
+ recordUpdateSpy.mockClear();
345
+
346
+ // Test batch processing with multiple documents
347
+ const batchInput = {
348
+ selectedDocs: Array.from({ length: 5 }, (_, i) => ({
349
+ path: `/docs/batch${i + 1}.md`,
350
+ content: `# Batch Document ${i + 1}`,
351
+ translates: {},
352
+ labels: {},
353
+ title: `Batch Document ${i + 1}`,
354
+ })),
355
+ docsDir: "./docs",
356
+ translateLanguages: ["en", "zh"],
357
+ locale: "en",
358
+ };
359
+
360
+ mockOptions.prompts.select.mockResolvedValue("yes");
361
+ mockOptions.context.invoke.mockResolvedValue({ mockResult: true });
362
+
363
+ const batchResult = await saveAndTranslateDocument(batchInput, mockOptions);
364
+
365
+ expect(batchResult).toEqual({});
366
+ // 5 documents * 3 calls each (save, translate, save) = 15 calls
367
+ expect(mockOptions.context.invoke).toHaveBeenCalledTimes(15);
368
+ });
369
+ });
@@ -15,7 +15,8 @@ import {
15
15
  } from "./blocklet.mjs";
16
16
  import {
17
17
  BLOCKLET_ADD_COMPONENT_DOCS,
18
- DEFAULT_APP_URL,
18
+ CLOUD_SERVICE_URL_PROD,
19
+ CLOUD_SERVICE_URL_STAGING,
19
20
  DISCUSS_KIT_DID,
20
21
  DISCUSS_KIT_STORE_URL,
21
22
  DOC_OFFICIAL_ACCESS_TOKEN,
@@ -98,7 +99,7 @@ export async function getAccessToken(appUrl, ltToken = "") {
98
99
  appLogo: "https://docsmith.aigne.io/image-bin/uploads/9645caf64b4232699982c4d940b03b90.svg",
99
100
  openPage: (pageUrl) => {
100
101
  const url = new URL(pageUrl);
101
- if (url.hostname !== DEFAULT_APP_URL) {
102
+ if ([CLOUD_SERVICE_URL_PROD, CLOUD_SERVICE_URL_STAGING].includes(url.origin) === false) {
102
103
  url.searchParams.set("required_roles", "owner,admin");
103
104
  }
104
105
  if (ltToken) {
@@ -335,7 +335,8 @@ export const PAYMENT_KIT_DID = "z2qaCNvKMv5GjouKdcDWexv6WqtHbpNPQDnAk";
335
335
  export const DOC_OFFICIAL_ACCESS_TOKEN = "DOC_OFFICIAL_ACCESS_TOKEN";
336
336
 
337
337
  // Default application URL for the document deployment website.
338
- export const DEFAULT_APP_URL = "https://docsmith.aigne.io";
338
+ export const CLOUD_SERVICE_URL_PROD = "https://docsmith.aigne.io";
339
+ export const CLOUD_SERVICE_URL_STAGING = "https://staging.docsmith.aigne.io";
339
340
 
340
341
  // Discuss Kit related URLs
341
342
  export const DISCUSS_KIT_STORE_URL =
package/utils/deploy.mjs CHANGED
@@ -2,11 +2,11 @@ import { BrokerClient, STEPS } from "@blocklet/payment-broker-client/node";
2
2
  import chalk from "chalk";
3
3
  import open from "open";
4
4
  import { getOfficialAccessToken } from "./auth-utils.mjs";
5
- import { DEFAULT_APP_URL } from "./constants/index.mjs";
5
+ import { CLOUD_SERVICE_URL_PROD } from "./constants/index.mjs";
6
6
  import { saveValueToConfig } from "./utils.mjs";
7
7
 
8
8
  // ==================== Configuration ====================
9
- const BASE_URL = process.env.DOC_SMITH_BASE_URL || DEFAULT_APP_URL;
9
+ const BASE_URL = process.env.DOC_SMITH_BASE_URL || CLOUD_SERVICE_URL_PROD;
10
10
  const SUCCESS_MESSAGE = {
11
11
  en: "Congratulations! Your website has been successfully installed. You can return to the command-line tool to continue the next steps.",
12
12
  zh: "恭喜您,你的网站已安装成功!可以返回命令行工具继续后续操作!",