@musnows/scriverse 0.9.8 → 0.9.10

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/app.js CHANGED
@@ -41,6 +41,8 @@ import { paginated, parsePagination } from "./pagination.js";
41
41
  import { normalizeUploadFileName } from "./utils.js";
42
42
  import { assertSafeAiEndpoint, assertSafeS3Endpoint, createApiRateLimitMiddleware, createAuthenticationRateLimitMiddleware, createBasicAuthMiddleware, createCaptchaRateLimitMiddleware, createExpensiveApiRateLimitMiddleware, createSameOriginMiddleware, createSecurityHeadersMiddleware, createUploadRateLimitMiddleware, enforceCaseInsensitiveRouting, normalizeApiPath, resolveTrustProxySetting, verifySetupToken } from "./security.js";
43
43
  import { ImageCaptchaService } from "./image-captcha.js";
44
+ import { ImService } from "./im.js";
45
+ import { ImOrchestrator } from "./im-orchestrator.js";
44
46
  import { OfflineSyncService } from "./offline-sync.js";
45
47
  import { assertSafeImportedPlainText, decodeUtf8ImportedText } from "./import-security.js";
46
48
  import { InvalidRasterImageError, readRasterImageMetadata } from "./image-metadata.js";
@@ -1293,6 +1295,7 @@ export function createRuntime(options) {
1293
1295
  ? auth.listUsers().find((user) => user.status === "active") ?? null
1294
1296
  : null;
1295
1297
  const store = new Store(database);
1298
+ const im = new ImService(store, auth);
1296
1299
  const offlineSync = new OfflineSyncService(database, store);
1297
1300
  const platformAiSettings = store.getPlatformAiSettings();
1298
1301
  const platformAiStreamIdleTimeoutMs = normalizeAiStreamIdleTimeoutSeconds(Number(platformAiSettings.streamIdleTimeoutSeconds)) * 1_000;
@@ -1395,6 +1398,7 @@ export function createRuntime(options) {
1395
1398
  liteLlmPriceCache,
1396
1399
  allowPrivateAiEndpoints: options.security?.allowPrivateAiEndpoints === true
1397
1400
  });
1401
+ const imOrchestrator = new ImOrchestrator(store, auth, im, ai);
1398
1402
  // AI 可写工具与审批工作流:计划创建只能由侧边栏 AI 发起,确认入口只接收审批 ID。
1399
1403
  const aiWritePlanManager = new AiWritePlanManager({
1400
1404
  database,
@@ -1526,6 +1530,7 @@ export function createRuntime(options) {
1526
1530
  profileId: input.profileId,
1527
1531
  clientVersion: input.clientVersion
1528
1532
  });
1533
+ imOrchestrator.disconnectUser(result.session.user.userId);
1529
1534
  response.setHeader("Cache-Control", "no-store");
1530
1535
  runWithRequestActor(result.session.user, () => store.audit(null, "user.logged-in", "user", result.session.user.userId, { source: "desktop" }));
1531
1536
  logger.info("auth.desktop_login.succeeded", { actorRef: accountReference(result.session.user.userId) });
@@ -1545,12 +1550,15 @@ export function createRuntime(options) {
1545
1550
  data(response, { authenticated: true, user: request.authUser, apiKeyPrefix: request.authApiKey?.prefix ?? null });
1546
1551
  });
1547
1552
  app.delete("/api/auth/session", (request, response) => {
1553
+ const userId = request.authUser?.userId;
1548
1554
  if (request.authDesktopSession)
1549
1555
  auth.revokeDesktop(request.authDesktopSession.id);
1550
1556
  if (request.authSession) {
1551
1557
  auth.revoke(request.authSession.id);
1552
1558
  clearSessionCookie(response, request.secure);
1553
1559
  }
1560
+ if (userId)
1561
+ imOrchestrator.disconnectUser(userId);
1554
1562
  noContent(response);
1555
1563
  });
1556
1564
  app.post("/api/auth/onboarding/complete", (request, response) => {
@@ -1614,6 +1622,7 @@ export function createRuntime(options) {
1614
1622
  throw new AppError(401, "AUTH_REQUIRED", "请先登录");
1615
1623
  const input = parse(passwordChangeSchema, request.body);
1616
1624
  auth.changePassword(request.authUser.userId, activeSession.id, input.currentPassword, input.newPassword, activeSession.kind);
1625
+ imOrchestrator.disconnectUser(request.authUser.userId);
1617
1626
  store.audit(null, "user.password-changed", "user", request.authUser.userId);
1618
1627
  noContent(response);
1619
1628
  });
@@ -1669,9 +1678,352 @@ export function createRuntime(options) {
1669
1678
  if (request.authUser.role !== "admin")
1670
1679
  throw new AppError(403, "ADMIN_REQUIRED", "该操作仅限系统管理员");
1671
1680
  const updated = auth.updateUser(request.authUser, request.params.userId, parse(userUpdateSchema, request.body));
1681
+ if (updated.status !== "active")
1682
+ imOrchestrator.disconnectUser(updated.userId);
1672
1683
  store.audit(null, "user.updated", "user", updated.userId, { role: updated.role, status: updated.status });
1673
1684
  data(response, updated);
1674
1685
  });
1686
+ const requireImUser = (request) => {
1687
+ if (!request.authUser || !hasInteractiveSession(request)) {
1688
+ throw new AppError(401, "SESSION_REQUIRED", "请使用交互式会话访问 IM");
1689
+ }
1690
+ return request.authUser;
1691
+ };
1692
+ const imSettingsSchema = z.object({
1693
+ preferredName: z.string().trim().max(80).optional(),
1694
+ pronouns: z.string().trim().max(80).optional(),
1695
+ identitySummary: z.string().trim().max(2000).optional(),
1696
+ additionalNotes: z.string().trim().max(4000).optional(),
1697
+ primaryModelId: identifier.nullable().optional(),
1698
+ fallbackModelId: identifier.nullable().optional(),
1699
+ retryCount: z.number().int().min(1).max(20).optional()
1700
+ }).strict();
1701
+ const imGroupSchema = z.object({
1702
+ title: nonEmpty.max(80),
1703
+ characterIds: z.array(identifier).min(1).max(10),
1704
+ humanUserIds: z.array(identifier).max(49).optional(),
1705
+ replyMode: z.enum(["mention", "proactive"]).optional(),
1706
+ responseThreshold: z.number().int().min(0).max(100).optional(),
1707
+ maxAiMessages: z.number().int().min(1).max(100).optional()
1708
+ }).strict();
1709
+ const imGroupUpdateSchema = imGroupSchema.pick({
1710
+ title: true,
1711
+ replyMode: true,
1712
+ responseThreshold: true,
1713
+ maxAiMessages: true
1714
+ }).partial().strict().refine((value) => Object.keys(value).length > 0, "至少提供一项要修改的群设置");
1715
+ app.get("/api/im/settings", (request, response) => {
1716
+ const user = requireImUser(request);
1717
+ data(response, im.getSettings(user.userId));
1718
+ });
1719
+ app.patch("/api/im/settings", (request, response) => {
1720
+ const user = requireImUser(request);
1721
+ data(response, im.updateSettings(user.userId, parse(imSettingsSchema, request.body)));
1722
+ });
1723
+ app.get("/api/im/models", (request, response) => {
1724
+ requireImUser(request);
1725
+ data(response, im.listModels());
1726
+ });
1727
+ app.get("/api/im/works", (request, response) => {
1728
+ const user = requireImUser(request);
1729
+ data(response, im.listAvailableWorks(user));
1730
+ });
1731
+ app.get("/api/im/characters", (request, response) => {
1732
+ const user = requireImUser(request);
1733
+ const query = parse(z.object({
1734
+ q: z.string().trim().max(100).default(""),
1735
+ workId: identifier.optional(),
1736
+ cursor: z.coerce.number().int().nonnegative().default(0),
1737
+ limit: z.coerce.number().int().min(1).max(100).default(50)
1738
+ }).strict(), request.query);
1739
+ const characters = im.listAvailableCharacters(user, query.q, query.workId, query.limit + 1, query.cursor);
1740
+ const hasMore = characters.length > query.limit;
1741
+ data(response, {
1742
+ items: characters.slice(0, query.limit),
1743
+ nextCursor: hasMore ? query.cursor + query.limit : null
1744
+ });
1745
+ });
1746
+ app.get("/api/im/conversations", (request, response) => {
1747
+ const user = requireImUser(request);
1748
+ const query = parse(z.object({
1749
+ cursor: z.string().max(500).optional(),
1750
+ limit: z.coerce.number().int().min(1).max(100).default(50)
1751
+ }).strict(), request.query);
1752
+ let cursor;
1753
+ if (query.cursor) {
1754
+ try {
1755
+ cursor = parse(z.object({
1756
+ updatedAt: z.string().datetime({ offset: true }),
1757
+ createdAt: z.string().datetime({ offset: true }),
1758
+ id: identifier
1759
+ }).strict(), JSON.parse(Buffer.from(query.cursor, "base64url").toString("utf8")));
1760
+ }
1761
+ catch {
1762
+ throw new AppError(400, "IM_CONVERSATION_CURSOR_INVALID", "IM 会话分页游标无效");
1763
+ }
1764
+ }
1765
+ const page = im.listConversations(user.userId, query.limit + 1, cursor);
1766
+ const items = page.slice(0, query.limit);
1767
+ const last = items.at(-1);
1768
+ const totals = im.conversationUnreadTotals(user.userId);
1769
+ data(response, {
1770
+ items,
1771
+ nextCursor: page.length > query.limit && last ? Buffer.from(JSON.stringify({
1772
+ updatedAt: last.updatedAt,
1773
+ createdAt: last.createdAt,
1774
+ id: last.id
1775
+ })).toString("base64url") : null,
1776
+ ...totals
1777
+ });
1778
+ });
1779
+ app.get("/api/im/unread", (request, response) => {
1780
+ const user = requireImUser(request);
1781
+ data(response, im.conversationUnreadTotals(user.userId));
1782
+ });
1783
+ app.get("/api/im/conversations/:conversationId/summary", (request, response) => {
1784
+ const user = requireImUser(request);
1785
+ data(response, im.getConversationSummary(request.params.conversationId, user.userId));
1786
+ });
1787
+ app.get("/api/im/events", (request, response) => {
1788
+ const user = requireImUser(request);
1789
+ let started = false;
1790
+ let closed = false;
1791
+ let heartbeat = null;
1792
+ let unsubscribe = () => undefined;
1793
+ const pendingEvents = [];
1794
+ const close = () => {
1795
+ if (closed)
1796
+ return;
1797
+ closed = true;
1798
+ if (heartbeat)
1799
+ clearInterval(heartbeat);
1800
+ unsubscribe();
1801
+ if (!response.writableEnded)
1802
+ response.end();
1803
+ };
1804
+ const write = (chunk) => {
1805
+ if (closed || response.destroyed || response.writableEnded) {
1806
+ close();
1807
+ return false;
1808
+ }
1809
+ try {
1810
+ if (response.write(chunk))
1811
+ return true;
1812
+ }
1813
+ catch {
1814
+ close();
1815
+ return false;
1816
+ }
1817
+ close();
1818
+ return false;
1819
+ };
1820
+ const writeEvent = (event) => {
1821
+ write(`id: ${event.id}\nevent: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`);
1822
+ };
1823
+ unsubscribe = imOrchestrator.subscribe(user.userId, (event) => {
1824
+ if (!started)
1825
+ pendingEvents.push(event);
1826
+ else
1827
+ writeEvent(event);
1828
+ }, close, request.authDesktopSession?.expiresAt ?? request.authSession?.expiresAt);
1829
+ response.status(200);
1830
+ response.setHeader("Content-Type", "text/event-stream; charset=utf-8");
1831
+ response.setHeader("Cache-Control", "no-cache, no-transform");
1832
+ response.setHeader("Connection", "keep-alive");
1833
+ response.flushHeaders();
1834
+ started = true;
1835
+ if (!write(`event: ready\ndata: ${JSON.stringify({ connected: true })}\n\n`))
1836
+ return;
1837
+ for (const event of pendingEvents) {
1838
+ if (closed)
1839
+ return;
1840
+ writeEvent(event);
1841
+ }
1842
+ pendingEvents.length = 0;
1843
+ heartbeat = setInterval(() => write(": heartbeat\n\n"), 15_000);
1844
+ heartbeat.unref();
1845
+ request.on("close", close);
1846
+ });
1847
+ app.post("/api/im/conversations/direct", (request, response) => {
1848
+ const user = requireImUser(request);
1849
+ const input = parse(z.object({ characterId: identifier }).strict(), request.body);
1850
+ const result = im.createDirectResult(user, input.characterId);
1851
+ if (result.created || result.changed)
1852
+ imOrchestrator.publishConversation(String(result.conversation.id));
1853
+ data(response, result.conversation, result.created ? 201 : 200);
1854
+ });
1855
+ app.post("/api/im/conversations/group", (request, response) => {
1856
+ const user = requireImUser(request);
1857
+ const conversation = im.createGroup(user, parse(imGroupSchema, request.body));
1858
+ imOrchestrator.publishConversation(String(conversation.id));
1859
+ data(response, conversation, 201);
1860
+ });
1861
+ app.get("/api/im/conversations/:conversationId", (request, response) => {
1862
+ const user = requireImUser(request);
1863
+ const query = parse(z.object({
1864
+ beforeSequence: z.coerce.number().int().positive().optional(),
1865
+ afterSequence: z.coerce.number().int().nonnegative().optional()
1866
+ }).strict().refine((value) => value.beforeSequence === undefined || value.afterSequence === undefined, {
1867
+ message: "beforeSequence 和 afterSequence 不能同时使用"
1868
+ }), request.query);
1869
+ const conversation = im.getConversation(request.params.conversationId, user.userId, query.beforeSequence, query.afterSequence);
1870
+ const activeChain = conversation.activeChain;
1871
+ data(response, {
1872
+ ...conversation,
1873
+ streamingReplies: conversation.active === true && activeChain
1874
+ ? imOrchestrator.streamingReplySnapshots(request.params.conversationId)
1875
+ .filter((snapshot) => snapshot.chainId === activeChain.id)
1876
+ : []
1877
+ });
1878
+ });
1879
+ app.patch("/api/im/conversations/:conversationId", (request, response) => {
1880
+ const user = requireImUser(request);
1881
+ const result = im.updateGroup(user, request.params.conversationId, parse(imGroupUpdateSchema, request.body));
1882
+ if (result.changed) {
1883
+ imOrchestrator.cancelConversation(request.params.conversationId, "group_settings_changed");
1884
+ imOrchestrator.publishConversation(request.params.conversationId);
1885
+ }
1886
+ data(response, result.conversation);
1887
+ });
1888
+ app.post("/api/im/conversations/:conversationId/humans", (request, response) => {
1889
+ const user = requireImUser(request);
1890
+ const input = parse(z.object({ userId: identifier }).strict(), request.body);
1891
+ const updated = im.addHuman(user, request.params.conversationId, input.userId);
1892
+ imOrchestrator.cancelConversation(request.params.conversationId, "human_member_joined");
1893
+ imOrchestrator.publishConversation(request.params.conversationId);
1894
+ data(response, updated, 201);
1895
+ });
1896
+ app.delete("/api/im/conversations/:conversationId/humans/:userId", (request, response) => {
1897
+ const user = requireImUser(request);
1898
+ im.removeHuman(user, request.params.conversationId, request.params.userId);
1899
+ imOrchestrator.cancelConversation(request.params.conversationId, "human_member_removed");
1900
+ imOrchestrator.publishConversation(request.params.conversationId);
1901
+ imOrchestrator.publishConversationToUser(request.params.userId, request.params.conversationId);
1902
+ noContent(response);
1903
+ });
1904
+ app.post("/api/im/conversations/:conversationId/leave", (request, response) => {
1905
+ const user = requireImUser(request);
1906
+ parse(z.object({}).strict(), request.body ?? {});
1907
+ im.leaveGroup(user, request.params.conversationId);
1908
+ imOrchestrator.cancelConversation(request.params.conversationId, "human_member_left");
1909
+ imOrchestrator.publishConversation(request.params.conversationId);
1910
+ imOrchestrator.publishConversationToUser(user.userId, request.params.conversationId);
1911
+ noContent(response);
1912
+ });
1913
+ app.post("/api/im/conversations/:conversationId/characters", (request, response) => {
1914
+ const user = requireImUser(request);
1915
+ const input = parse(z.object({ characterId: identifier }).strict(), request.body);
1916
+ const updated = im.addCharacter(user, request.params.conversationId, input.characterId);
1917
+ imOrchestrator.cancelConversation(request.params.conversationId, "character_member_joined");
1918
+ imOrchestrator.publishConversation(request.params.conversationId);
1919
+ data(response, updated, 201);
1920
+ });
1921
+ app.delete("/api/im/conversations/:conversationId/characters/:characterId", (request, response) => {
1922
+ const user = requireImUser(request);
1923
+ im.removeCharacter(user, request.params.conversationId, request.params.characterId);
1924
+ imOrchestrator.cancelConversation(request.params.conversationId, "character_member_removed");
1925
+ imOrchestrator.publishConversation(request.params.conversationId);
1926
+ noContent(response);
1927
+ });
1928
+ app.get("/api/im/conversations/:conversationId/characters/:characterId/avatar", async (request, response) => {
1929
+ const user = requireImUser(request);
1930
+ const query = parse(z.object({ v: z.string().regex(/^[a-f0-9]{64}$/u).optional() }).strict(), request.query);
1931
+ const avatar = im.getCharacterAvatarVersionAccess(user.userId, request.params.conversationId, request.params.characterId, query.v);
1932
+ const content = await characterAvatarStorage.read(String(avatar.storageKey));
1933
+ response.setHeader("Content-Type", String(avatar.mimeType));
1934
+ response.setHeader("Content-Length", String(content.byteLength));
1935
+ response.setHeader("ETag", `\"${String(avatar.sha256)}\"`);
1936
+ response.setHeader("Cache-Control", "private, max-age=31536000, immutable");
1937
+ response.setHeader("X-Content-Type-Options", "nosniff");
1938
+ response.send(content);
1939
+ });
1940
+ app.get("/api/im/conversations/:conversationId/users/:userId/avatar", (request, response) => {
1941
+ const user = requireImUser(request);
1942
+ const query = parse(z.object({ v: z.string().regex(/^[a-f0-9]{64}$/u) }).strict(), request.query);
1943
+ const avatar = im.getHumanAvatarVersionAccess(user.userId, request.params.conversationId, request.params.userId, query.v);
1944
+ response.setHeader("Content-Type", String(avatar.mimeType));
1945
+ response.setHeader("Content-Length", String(avatar.byteLength));
1946
+ response.setHeader("ETag", `\"${String(avatar.sha256)}\"`);
1947
+ response.setHeader("Cache-Control", "private, max-age=31536000, immutable");
1948
+ response.setHeader("X-Content-Type-Options", "nosniff");
1949
+ response.send(avatar.content);
1950
+ });
1951
+ app.post("/api/im/conversations/:conversationId/transfer", (request, response) => {
1952
+ const user = requireImUser(request);
1953
+ const input = parse(z.object({ userId: identifier }).strict(), request.body);
1954
+ const updated = im.transferGroup(user, request.params.conversationId, input.userId);
1955
+ imOrchestrator.cancelConversation(request.params.conversationId, "group_owner_transferred");
1956
+ imOrchestrator.publishConversation(request.params.conversationId);
1957
+ data(response, updated);
1958
+ });
1959
+ app.post("/api/im/conversations/:conversationId/disband", (request, response) => {
1960
+ const user = requireImUser(request);
1961
+ parse(z.object({}).strict(), request.body ?? {});
1962
+ const memberUserIds = im.disbandGroup(user, request.params.conversationId);
1963
+ imOrchestrator.cancelConversation(request.params.conversationId, "group_disbanded");
1964
+ for (const userId of memberUserIds)
1965
+ imOrchestrator.publishConversationToUser(userId, request.params.conversationId);
1966
+ imOrchestrator.forgetConversation(request.params.conversationId);
1967
+ noContent(response);
1968
+ });
1969
+ app.post("/api/im/conversations/:conversationId/messages", (request, response) => {
1970
+ const user = requireImUser(request);
1971
+ const input = parse(z.object({
1972
+ content: z.string().trim().min(1).max(20_000),
1973
+ requestId: idempotencyKeySchema
1974
+ }).strict(), request.body);
1975
+ const current = im.getConversation(request.params.conversationId, user.userId);
1976
+ if (current.active !== true)
1977
+ throw new AppError(403, "IM_MEMBERSHIP_INACTIVE", "你已经退出这个 IM 会话");
1978
+ const result = im.sendMessage(user, request.params.conversationId, input, (chainId) => {
1979
+ imOrchestrator.abortConversationRuns(request.params.conversationId, "human_message_received", chainId);
1980
+ });
1981
+ imOrchestrator.publishMessageResult(result);
1982
+ data(response, result, 201);
1983
+ });
1984
+ app.post("/api/im/conversations/:conversationId/announcements", (request, response) => {
1985
+ const user = requireImUser(request);
1986
+ const input = parse(z.object({
1987
+ content: z.string().trim().min(1).max(20_000),
1988
+ requestId: idempotencyKeySchema
1989
+ }).strict(), request.body);
1990
+ const result = im.publishAnnouncement(user, request.params.conversationId, input);
1991
+ imOrchestrator.publishMessageResult(result);
1992
+ data(response, result, 201);
1993
+ });
1994
+ app.post("/api/im/conversations/:conversationId/read", (request, response) => {
1995
+ const user = requireImUser(request);
1996
+ const input = parse(z.object({ sequence: z.number().int().min(0) }).strict(), request.body);
1997
+ const result = im.markRead(user.userId, request.params.conversationId, input.sequence);
1998
+ if (result.changed)
1999
+ imOrchestrator.publishConversationToUser(user.userId, request.params.conversationId);
2000
+ data(response, result.summary);
2001
+ });
2002
+ app.post("/api/im/conversations/:conversationId/stop", (request, response) => {
2003
+ const user = requireImUser(request);
2004
+ parse(z.object({}).strict(), request.body ?? {});
2005
+ const current = im.getConversation(request.params.conversationId, user.userId);
2006
+ if (current.active !== true)
2007
+ throw new AppError(403, "IM_MEMBERSHIP_INACTIVE", "你已经退出这个 IM 会话");
2008
+ im.stopChain(user.userId, request.params.conversationId);
2009
+ imOrchestrator.cancelConversation(request.params.conversationId, "stopped_by_user");
2010
+ noContent(response);
2011
+ });
2012
+ app.post("/api/im/conversations/:conversationId/chains/:chainId/retry", (request, response) => {
2013
+ const user = requireImUser(request);
2014
+ parse(z.object({}).strict(), request.body ?? {});
2015
+ const chain = im.retryChain(user, request.params.conversationId, request.params.chainId, (chainId) => {
2016
+ imOrchestrator.abortConversationRuns(request.params.conversationId, "manual_retry", chainId);
2017
+ });
2018
+ if (String(chain.status) === "queued")
2019
+ imOrchestrator.enqueue(String(chain.id));
2020
+ imOrchestrator.publishConversation(request.params.conversationId);
2021
+ data(response, chain, 201);
2022
+ });
2023
+ app.get("/api/im/conversations/:conversationId/diagnostics", (request, response) => {
2024
+ const user = requireImUser(request);
2025
+ data(response, im.getDiagnostics(user, request.params.conversationId));
2026
+ });
1675
2027
  app.get("/api/works", (request, response) => {
1676
2028
  const pagination = parsePagination(request.query);
1677
2029
  data(response, pagination ? store.listWorksPage(pagination) : store.listWorks());
@@ -3006,6 +3358,12 @@ export function createRuntime(options) {
3006
3358
  const permissions = requestPermissions(request, String(updated.workId));
3007
3359
  data(response, redactAiConversation(updated, permissions));
3008
3360
  });
3361
+ app.patch("/api/ai-conversations/:conversationId/title", (request, response) => {
3362
+ const input = parse(z.object({ title: nonEmpty.max(200) }).strict(), request.body);
3363
+ const updated = store.setAiConversationTitle(request.params.conversationId, input.title);
3364
+ const permissions = requestPermissions(request, String(updated.workId));
3365
+ data(response, redactAiConversation(updated, permissions));
3366
+ });
3009
3367
  app.delete("/api/ai-conversations/:conversationId", (request, response) => {
3010
3368
  store.deleteAiConversation(request.params.conversationId);
3011
3369
  data(response, { deleted: true });
@@ -3226,6 +3584,9 @@ export function createRuntime(options) {
3226
3584
  answerText: String(continuation.answerText ?? ""),
3227
3585
  selectedOptionLabel: typeof continuation.selectedOptionLabel === "string" ? continuation.selectedOptionLabel : null,
3228
3586
  supplementalAnswer: typeof continuation.customAnswer === "string" ? continuation.customAnswer : "",
3587
+ ...(Array.isArray(continuation.answers)
3588
+ ? { answers: continuation.answers.filter((answer) => Boolean(answer) && typeof answer === "object" && !Array.isArray(answer)) }
3589
+ : {}),
3229
3590
  ...(typeof continuation.modelId === "string" && continuation.modelId ? { modelId: continuation.modelId } : {}),
3230
3591
  ...(typeof continuation.toolCallId === "string" && continuation.toolCallId ? { toolCallId: continuation.toolCallId } : {}),
3231
3592
  ...(typeof continuation.assistantMessageRequestId === "string" && continuation.assistantMessageRequestId
@@ -3247,7 +3608,7 @@ export function createRuntime(options) {
3247
3608
  app.post("/api/works/:workId/ai/questions/:questionId/answer", async (request, response) => {
3248
3609
  const input = parse(answerAiUserQuestionSchema, request.body ?? {});
3249
3610
  const viewer = planViewer();
3250
- aiWritePlanManager.answerQuestion(request.params.questionId, request.params.workId, viewer, { ...(input.selectedOption !== undefined ? { selectedOption: input.selectedOption } : {}), ...(input.customAnswer !== undefined ? { customAnswer: input.customAnswer } : {}) });
3611
+ aiWritePlanManager.answerQuestion(request.params.questionId, request.params.workId, viewer, input);
3251
3612
  await resumeQuestionWorkflow(request.params.questionId, request.params.workId, viewer);
3252
3613
  data(response, aiWritePlanManager.getQuestion(request.params.questionId, request.params.workId, viewer));
3253
3614
  });
@@ -3964,6 +4325,7 @@ export function createRuntime(options) {
3964
4325
  backups.startScheduler();
3965
4326
  logger.info("runtime.ready", { serveUi: options.serveUi ?? true });
3966
4327
  let closePromise = null;
4328
+ let imOrchestratorClosePromise = null;
3967
4329
  let stopping = false;
3968
4330
  let closed = false;
3969
4331
  const close = () => {
@@ -3976,6 +4338,7 @@ export function createRuntime(options) {
3976
4338
  logger.info("runtime.closing");
3977
4339
  backups.dispose();
3978
4340
  liteLlmPriceCache.dispose();
4341
+ imOrchestratorClosePromise = imOrchestrator.dispose();
3979
4342
  ai.dispose();
3980
4343
  offlineSync.dispose();
3981
4344
  const cancelledStreamRequests = store.cancelActiveAiConversationStreamRequests();
@@ -3985,6 +4348,7 @@ export function createRuntime(options) {
3985
4348
  closePromise = (async () => {
3986
4349
  try {
3987
4350
  await backups.waitForIdle(RUNTIME_BACKUP_IDLE_TIMEOUT_MS);
4351
+ await imOrchestratorClosePromise;
3988
4352
  collaborationPresence.close();
3989
4353
  database.close();
3990
4354
  if (temporaryStorageRoot)
@@ -4003,6 +4367,6 @@ export function createRuntime(options) {
4003
4367
  })();
4004
4368
  return closePromise;
4005
4369
  };
4006
- return { app, database, store, ai, liteLlmPriceCache, backups, auth, offlineSync, attachmentStorage, characterAvatarStorage, cleanupAttachments, close };
4370
+ return { app, database, store, ai, liteLlmPriceCache, backups, auth, im, imOrchestrator, offlineSync, attachmentStorage, characterAvatarStorage, cleanupAttachments, close };
4007
4371
  }
4008
4372
  //# sourceMappingURL=app.js.map