@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/ai-write-plans.js +136 -48
- package/dist/ai-write-plans.js.map +1 -1
- package/dist/ai.js +404 -100
- package/dist/ai.js.map +1 -1
- package/dist/app.js +366 -2
- package/dist/app.js.map +1 -1
- package/dist/cli-core.js +95 -3
- package/dist/cli-core.js.map +1 -1
- package/dist/database.js +549 -4
- package/dist/database.js.map +1 -1
- package/dist/im-orchestrator.js +1317 -0
- package/dist/im-orchestrator.js.map +1 -0
- package/dist/im.js +1690 -0
- package/dist/im.js.map +1 -0
- package/dist/public/ai-interactive.js +14 -13
- package/dist/public/app.js +290 -84
- package/dist/public/im.d.ts +71 -0
- package/dist/public/im.js +1899 -0
- package/dist/public/index.html +108 -5
- package/dist/public/keyboard-shortcuts.d.ts +1 -0
- package/dist/public/keyboard-shortcuts.js +9 -0
- package/dist/public/page-route.d.ts +2 -1
- package/dist/public/page-route.js +4 -1
- package/dist/public/styles.css +336 -4
- package/dist/s3-backup.js +5 -1
- package/dist/s3-backup.js.map +1 -1
- package/dist/security.js +1 -0
- package/dist/security.js.map +1 -1
- package/dist/store.js +4 -1
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +9 -4
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
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,
|
|
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
|