weifuwu 0.60.0 → 0.61.0

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.
Files changed (38) hide show
  1. package/README.md +23 -7
  2. package/dist/ai/agent.d.ts +29 -1
  3. package/dist/ai/client.d.ts +15 -0
  4. package/dist/ai/index.d.ts +2 -1
  5. package/dist/client/diff.d.ts +15 -0
  6. package/dist/client/hydration.d.ts +13 -0
  7. package/dist/client/index.d.ts +4 -0
  8. package/dist/client/index.js +3 -2214
  9. package/dist/client/middleware/api.d.ts +2 -0
  10. package/dist/client/motion.d.ts +8 -0
  11. package/dist/client/registry.d.ts +34 -0
  12. package/dist/client/render.d.ts +13 -14
  13. package/dist/client/types.d.ts +28 -0
  14. package/dist/client/ui.d.ts +56 -0
  15. package/dist/components/Badge/Badge.d.ts +1 -1
  16. package/dist/components/Button/Button.d.ts +3 -0
  17. package/dist/components/Card/Card.d.ts +2 -0
  18. package/dist/components/Confirm/Confirm.d.ts +8 -1
  19. package/dist/components/Dropdown/Dropdown.d.ts +2 -0
  20. package/dist/components/Icon/Icon.d.ts +19 -0
  21. package/dist/components/Input/Input.d.ts +5 -0
  22. package/dist/components/Modal/Modal.d.ts +2 -0
  23. package/dist/components/PageHeader/PageHeader.d.ts +2 -0
  24. package/dist/components/StatCard/StatCard.d.ts +2 -0
  25. package/dist/components/Table/Table.d.ts +4 -0
  26. package/dist/components/Toast/Toast.d.ts +13 -1
  27. package/dist/components/index.d.ts +2 -0
  28. package/dist/components/index.js +1 -3381
  29. package/dist/components/style.css +282 -134
  30. package/dist/core/router.d.ts +10 -10
  31. package/dist/core/serve.d.ts +1 -1
  32. package/dist/index.d.ts +3 -1
  33. package/dist/index.js +523 -11
  34. package/dist/layout/weifuwu-layout.css +106 -17
  35. package/dist/messager/index.d.ts +107 -0
  36. package/dist/types.d.ts +2 -2
  37. package/dist/user/index.d.ts +3 -2
  38. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1513,7 +1513,11 @@ function userSystem(options) {
1513
1513
  return rows.length ? rows[0] : null;
1514
1514
  }
1515
1515
  async function issueSession(user) {
1516
- const token = signToken({ sub: user.id }, secret, accessTtlSeconds);
1516
+ const token = signToken(
1517
+ { sub: user.id, ...user.tenant ? { tenantId: user.tenant } : {} },
1518
+ secret,
1519
+ accessTtlSeconds
1520
+ );
1517
1521
  const refreshToken = generateRefreshToken();
1518
1522
  const expiresAt = new Date(Date.now() + refreshTtlDays * 24 * 3600 * 1e3);
1519
1523
  await sql.unsafe(
@@ -1539,15 +1543,26 @@ function userSystem(options) {
1539
1543
  }
1540
1544
  const mw = (async (req, ctx, next) => {
1541
1545
  currentUser = null;
1546
+ const sessionPayload = {};
1547
+ let payloadTenantId;
1542
1548
  const authHeader = req.headers.get("authorization");
1543
1549
  if (authHeader?.startsWith("Bearer ")) {
1544
1550
  const payload = verifyToken(authHeader.slice(7), secret);
1545
1551
  if (payload?.sub) {
1546
1552
  currentUser = await findUserById(String(payload.sub));
1553
+ for (const [k, v] of Object.entries(payload)) {
1554
+ if (k === "sub" || k === "iat" || k === "exp" || k === "type") continue;
1555
+ sessionPayload[k] = v;
1556
+ }
1557
+ sessionPayload.userId = String(payload.sub);
1558
+ payloadTenantId = payload.tenantId;
1547
1559
  }
1548
1560
  }
1549
1561
  ctx.user = currentUser;
1562
+ if (payloadTenantId != null) ctx.tenantId = String(payloadTenantId);
1550
1563
  ctx.auth = {
1564
+ // ── 会话 payload 字段(来自 token,应用层签发时决定带什么) ──
1565
+ ...sessionPayload,
1551
1566
  async register(input) {
1552
1567
  if (!input.email || !input.password) throw new HttpError("email and password are required", 400);
1553
1568
  if (input.password.length < 8) throw new HttpError("password must be at least 8 characters", 400);
@@ -1630,24 +1645,25 @@ function userSystem(options) {
1630
1645
  };
1631
1646
  mw.routes = (app, routeOpts) => {
1632
1647
  const p = routeOpts?.prefix ?? prefix;
1633
- app.post(`${p}/register`, async (req, ctx) => {
1648
+ const excluded = new Set(routeOpts?.exclude ?? []);
1649
+ if (!excluded.has("register")) app.post(`${p}/register`, async (req, ctx) => {
1634
1650
  const body = await req.json().catch(() => ({}));
1635
1651
  const result = await ctx.auth.register(body);
1636
1652
  return created(result);
1637
1653
  });
1638
- app.post(`${p}/login`, async (req, ctx) => {
1654
+ if (!excluded.has("login")) app.post(`${p}/login`, async (req, ctx) => {
1639
1655
  const body = await req.json().catch(() => ({}));
1640
1656
  if (!body.email || !body.password) return badRequest("email and password are required");
1641
1657
  const result = await ctx.auth.login(body.email, body.password);
1642
1658
  return ok(result);
1643
1659
  });
1644
- app.post(`${p}/logout`, async (req, ctx) => {
1660
+ if (!excluded.has("logout")) app.post(`${p}/logout`, async (req, ctx) => {
1645
1661
  const body = await req.json().catch(() => ({}));
1646
1662
  if (!body.refreshToken) return badRequest("refreshToken is required");
1647
1663
  await ctx.auth.logout(body.refreshToken);
1648
1664
  return noContent();
1649
1665
  });
1650
- app.post(`${p}/refresh`, async (req, ctx) => {
1666
+ if (!excluded.has("refresh")) app.post(`${p}/refresh`, async (req, ctx) => {
1651
1667
  const body = await req.json().catch(() => ({}));
1652
1668
  if (!body.refreshToken) throw new HttpError("refreshToken is required", 400);
1653
1669
  const user = await consumeRefreshToken(body.refreshToken);
@@ -1655,7 +1671,7 @@ function userSystem(options) {
1655
1671
  const session = await issueSession(user);
1656
1672
  return ok({ ...session, user });
1657
1673
  });
1658
- app.get(`${p}/me`, async (_req, ctx) => {
1674
+ if (!excluded.has("me")) app.get(`${p}/me`, async (_req, ctx) => {
1659
1675
  if (!ctx.user) throw new HttpError("Unauthorized", 401);
1660
1676
  return ok(ctx.user);
1661
1677
  });
@@ -1663,6 +1679,403 @@ function userSystem(options) {
1663
1679
  return mw;
1664
1680
  }
1665
1681
 
1682
+ // src/messager/index.ts
1683
+ var CONVERSATIONS = "_weifuwu_conversations";
1684
+ var MEMBERS = "_weifuwu_conversation_members";
1685
+ var MESSAGES = "_weifuwu_messages";
1686
+ function messager(options) {
1687
+ const sql = options.sql;
1688
+ const prefix = options.prefix ?? "/api/messages";
1689
+ async function createConversation(userId, input) {
1690
+ if (input.type === "direct") {
1691
+ const existing = await sql.unsafe(
1692
+ `SELECT c.id, c.type, c.created_by, c.created_at FROM ${CONVERSATIONS} c
1693
+ WHERE c.type = 'direct'
1694
+ AND EXISTS (SELECT 1 FROM ${MEMBERS} m WHERE m.conversation_id = c.id AND m.user_id = $1)
1695
+ AND EXISTS (SELECT 1 FROM ${MEMBERS} m WHERE m.conversation_id = c.id AND m.user_id = $2)
1696
+ AND (SELECT count(*) FROM ${MEMBERS} m WHERE m.conversation_id = c.id) = 2
1697
+ LIMIT 1`,
1698
+ [userId, input.otherUserId]
1699
+ );
1700
+ if (existing.length) return existing[0];
1701
+ return createConversationRow(userId, "direct", [userId, input.otherUserId]);
1702
+ }
1703
+ const memberIds = [userId, ...input.memberIds.filter((m) => m !== userId)];
1704
+ return createConversationRow(userId, "group", memberIds);
1705
+ }
1706
+ async function createConversationRow(createdBy, type, memberIds) {
1707
+ await sql.unsafe(`BEGIN`);
1708
+ try {
1709
+ const rows = await sql.unsafe(
1710
+ `INSERT INTO ${CONVERSATIONS} (type, created_by) VALUES ($1, $2) RETURNING id, type, created_by, created_at`,
1711
+ [type, createdBy]
1712
+ );
1713
+ const conv = rows[0];
1714
+ for (const memberId of memberIds) {
1715
+ await sql.unsafe(
1716
+ `INSERT INTO ${MEMBERS} (conversation_id, user_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`,
1717
+ [conv.id, memberId]
1718
+ );
1719
+ }
1720
+ await sql.unsafe(`COMMIT`);
1721
+ return conv;
1722
+ } catch (err) {
1723
+ await sql.unsafe(`ROLLBACK`);
1724
+ throw err;
1725
+ }
1726
+ }
1727
+ async function listConversations(userId) {
1728
+ const rows = await sql.unsafe(
1729
+ `SELECT c.id, c.type, c.created_by, c.created_at,
1730
+ (SELECT to_jsonb(m) FROM ${MESSAGES} m
1731
+ WHERE m.conversation_id = c.id AND m.deleted_at IS NULL
1732
+ ORDER BY m.created_at DESC, m.id DESC LIMIT 1) AS last_message,
1733
+ (SELECT count(*) FROM ${MESSAGES} m
1734
+ WHERE m.conversation_id = c.id AND m.deleted_at IS NULL
1735
+ AND m.sender_id IS DISTINCT FROM $1
1736
+ AND m.created_at > COALESCE(mem.last_read_at, 'epoch'::timestamptz)) AS unread_count
1737
+ FROM ${CONVERSATIONS} c
1738
+ JOIN ${MEMBERS} mem ON mem.conversation_id = c.id
1739
+ WHERE mem.user_id = $1
1740
+ ORDER BY c.created_at DESC`,
1741
+ [userId]
1742
+ );
1743
+ return rows.map((r) => {
1744
+ const conv = r;
1745
+ const last = conv.last_message;
1746
+ return {
1747
+ id: conv.id,
1748
+ type: conv.type,
1749
+ created_by: conv.created_by,
1750
+ created_at: new Date(conv.created_at).toISOString(),
1751
+ last_message: last && typeof last === "object" ? normalizeMessage(last) : null,
1752
+ unread_count: Number(conv.unread_count)
1753
+ };
1754
+ });
1755
+ }
1756
+ async function getConversationForUser(conversationId, userId) {
1757
+ if (!await isMember(conversationId, userId)) return null;
1758
+ const rows = await sql.unsafe(
1759
+ `SELECT id, type, created_by, created_at FROM ${CONVERSATIONS} WHERE id = $1`,
1760
+ [conversationId]
1761
+ );
1762
+ return rows.length ? rows[0] : null;
1763
+ }
1764
+ async function isMember(conversationId, userId) {
1765
+ const rows = await sql.unsafe(
1766
+ `SELECT 1 FROM ${MEMBERS} WHERE conversation_id = $1 AND user_id = $2`,
1767
+ [conversationId, userId]
1768
+ );
1769
+ return rows.length > 0;
1770
+ }
1771
+ async function sendMessage(conversationId, input) {
1772
+ const rows = await sql.unsafe(
1773
+ `INSERT INTO ${MESSAGES} (conversation_id, sender_type, sender_id, content, msg_type)
1774
+ VALUES ($1, $2, $3, $4, $5) RETURNING *`,
1775
+ [conversationId, input.senderType, input.senderId ?? null, input.content, input.msgType ?? "text"]
1776
+ );
1777
+ return normalizeMessage(rows[0]);
1778
+ }
1779
+ async function listMessages(conversationId, opts) {
1780
+ const limit = opts.limit ?? 50;
1781
+ const rows = await sql.unsafe(
1782
+ `SELECT * FROM ${MESSAGES}
1783
+ WHERE conversation_id = $1
1784
+ AND ($2::uuid IS NULL OR (created_at, id) < (SELECT created_at, id FROM ${MESSAGES} WHERE id = $2))
1785
+ ORDER BY created_at DESC, id DESC
1786
+ LIMIT $3`,
1787
+ [conversationId, opts.before ?? null, limit]
1788
+ );
1789
+ return rows.map(normalizeMessage);
1790
+ }
1791
+ async function editMessage(messageId, content) {
1792
+ const rows = await sql.unsafe(
1793
+ `UPDATE ${MESSAGES} SET content = $2, edited_at = now()
1794
+ WHERE id = $1 AND deleted_at IS NULL RETURNING *`,
1795
+ [messageId, content]
1796
+ );
1797
+ return rows.length ? normalizeMessage(rows[0]) : null;
1798
+ }
1799
+ async function deleteMessage(messageId) {
1800
+ const rows = await sql.unsafe(
1801
+ `UPDATE ${MESSAGES} SET deleted_at = now() WHERE id = $1 AND deleted_at IS NULL RETURNING id`,
1802
+ [messageId]
1803
+ );
1804
+ return rows.length > 0;
1805
+ }
1806
+ async function markRead(conversationId, userId) {
1807
+ await sql.unsafe(
1808
+ `UPDATE ${MEMBERS} SET last_read_at = now() WHERE conversation_id = $1 AND user_id = $2`,
1809
+ [conversationId, userId]
1810
+ );
1811
+ }
1812
+ function normalizeMessage(row) {
1813
+ return {
1814
+ id: row.id,
1815
+ conversation_id: row.conversation_id,
1816
+ sender_type: row.sender_type,
1817
+ sender_id: row.sender_id,
1818
+ content: row.content,
1819
+ msg_type: row.msg_type,
1820
+ created_at: new Date(row.created_at).toISOString(),
1821
+ edited_at: row.edited_at ? new Date(row.edited_at).toISOString() : null,
1822
+ deleted_at: row.deleted_at ? new Date(row.deleted_at).toISOString() : null
1823
+ };
1824
+ }
1825
+ const REDIS_PREFIX = "wf:msg:";
1826
+ const rooms = /* @__PURE__ */ new Map();
1827
+ const wsRooms = /* @__PURE__ */ new Map();
1828
+ let redisSub = null;
1829
+ function initRedis() {
1830
+ const pool = options.redis?.redis;
1831
+ if (!pool || redisSub) return;
1832
+ redisSub = pool.createSubscriber();
1833
+ redisSub.connect().then(() => {
1834
+ redisSub.psubscribe(`${REDIS_PREFIX}*`, (channel, message) => {
1835
+ const room = channel.slice(REDIS_PREFIX.length);
1836
+ const event = JSON.parse(message);
1837
+ broadcastLocal(room, event);
1838
+ });
1839
+ }).catch((err) => {
1840
+ console.error("[messager] redis subscriber init error:", err);
1841
+ });
1842
+ }
1843
+ function broadcastLocal(room, event) {
1844
+ const members = rooms.get(room);
1845
+ if (!members) return;
1846
+ const payload = JSON.stringify(event);
1847
+ for (const ws of [...members]) {
1848
+ if (ws.readyState === ws.OPEN) {
1849
+ try {
1850
+ ws.send(payload);
1851
+ } catch {
1852
+ }
1853
+ }
1854
+ }
1855
+ }
1856
+ function broadcast(room, event) {
1857
+ broadcastLocal(room, event);
1858
+ const pool = options.redis?.redis;
1859
+ if (pool) {
1860
+ pool.publish(`${REDIS_PREFIX}${room}`, JSON.stringify(event)).catch((err) => {
1861
+ console.error("[messager] redis publish error:", err);
1862
+ });
1863
+ }
1864
+ }
1865
+ function sendTo(userId, event) {
1866
+ broadcast(`user:${userId}`, event);
1867
+ }
1868
+ function join(room, ws) {
1869
+ let members = rooms.get(room);
1870
+ if (!members) {
1871
+ members = /* @__PURE__ */ new Set();
1872
+ rooms.set(room, members);
1873
+ }
1874
+ members.add(ws);
1875
+ let joined = wsRooms.get(ws);
1876
+ if (!joined) {
1877
+ joined = /* @__PURE__ */ new Set();
1878
+ wsRooms.set(ws, joined);
1879
+ }
1880
+ joined.add(room);
1881
+ }
1882
+ function leave(room, ws) {
1883
+ rooms.get(room)?.delete(ws);
1884
+ if (rooms.get(room)?.size === 0) rooms.delete(room);
1885
+ wsRooms.get(ws)?.delete(room);
1886
+ }
1887
+ function leaveAll(ws) {
1888
+ const joined = wsRooms.get(ws);
1889
+ if (joined) for (const room of [...joined]) leave(room, ws);
1890
+ wsRooms.delete(ws);
1891
+ }
1892
+ function handler() {
1893
+ return {
1894
+ open(ws) {
1895
+ ws.send(JSON.stringify({ type: "connected" }));
1896
+ },
1897
+ message(ws, _ctx, data) {
1898
+ try {
1899
+ const msg = JSON.parse(data.toString());
1900
+ if (msg.type === "ping") {
1901
+ ws.send(JSON.stringify({ type: "pong" }));
1902
+ } else if (msg.type === "subscribe" && msg.room) {
1903
+ join(msg.room, ws);
1904
+ ws.send(JSON.stringify({ type: "subscribed", room: msg.room }));
1905
+ } else if (msg.type === "unsubscribe" && msg.room) {
1906
+ leave(msg.room, ws);
1907
+ }
1908
+ } catch {
1909
+ }
1910
+ },
1911
+ close(ws) {
1912
+ leaveAll(ws);
1913
+ },
1914
+ error(ws) {
1915
+ leaveAll(ws);
1916
+ }
1917
+ };
1918
+ }
1919
+ async function close() {
1920
+ if (redisSub) {
1921
+ try {
1922
+ await redisSub.close?.();
1923
+ } catch {
1924
+ }
1925
+ redisSub = null;
1926
+ }
1927
+ rooms.clear();
1928
+ wsRooms.clear();
1929
+ }
1930
+ const client = {
1931
+ handler,
1932
+ broadcast,
1933
+ sendTo,
1934
+ join,
1935
+ leave,
1936
+ close,
1937
+ createConversation,
1938
+ listConversations,
1939
+ getConversationForUser,
1940
+ isMember,
1941
+ sendMessage,
1942
+ listMessages,
1943
+ editMessage,
1944
+ deleteMessage,
1945
+ markRead
1946
+ };
1947
+ initRedis();
1948
+ const mw = (async (req, ctx, next) => {
1949
+ ctx.msg = client;
1950
+ return next(req, ctx);
1951
+ });
1952
+ mw.client = client;
1953
+ mw.__meta = { injects: ["msg"], depends: ["sql"] };
1954
+ mw.migrate = async () => {
1955
+ await sql.unsafe(`
1956
+ CREATE TABLE IF NOT EXISTS ${CONVERSATIONS} (
1957
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1958
+ type TEXT NOT NULL DEFAULT 'direct',
1959
+ created_by UUID,
1960
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
1961
+ )
1962
+ `);
1963
+ await sql.unsafe(`
1964
+ CREATE TABLE IF NOT EXISTS ${MEMBERS} (
1965
+ conversation_id UUID NOT NULL REFERENCES ${CONVERSATIONS}(id) ON DELETE CASCADE,
1966
+ user_id UUID NOT NULL,
1967
+ last_read_at TIMESTAMPTZ,
1968
+ joined_at TIMESTAMPTZ NOT NULL DEFAULT now(),
1969
+ PRIMARY KEY (conversation_id, user_id)
1970
+ )
1971
+ `);
1972
+ await sql.unsafe(`
1973
+ CREATE TABLE IF NOT EXISTS ${MESSAGES} (
1974
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1975
+ conversation_id UUID NOT NULL REFERENCES ${CONVERSATIONS}(id) ON DELETE CASCADE,
1976
+ sender_type TEXT NOT NULL DEFAULT 'user',
1977
+ sender_id UUID,
1978
+ content TEXT NOT NULL,
1979
+ msg_type TEXT NOT NULL DEFAULT 'text',
1980
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
1981
+ edited_at TIMESTAMPTZ,
1982
+ deleted_at TIMESTAMPTZ
1983
+ )
1984
+ `);
1985
+ await sql.unsafe(`
1986
+ CREATE INDEX IF NOT EXISTS idx_messages_conv ON ${MESSAGES} (conversation_id, created_at DESC, id DESC)
1987
+ `);
1988
+ };
1989
+ mw.routes = (app, routeOpts) => {
1990
+ const p = routeOpts?.prefix ?? prefix;
1991
+ async function requireMember(conversationId, ctx) {
1992
+ if (!ctx.user) throw new HttpError("Unauthorized", 401);
1993
+ if (!await isMember(conversationId, ctx.user.id)) {
1994
+ throw new HttpError("Forbidden: not a conversation member", 403);
1995
+ }
1996
+ }
1997
+ app.post(`${p}/conversations`, async (req, ctx) => {
1998
+ if (!ctx.user) throw new HttpError("Unauthorized", 401);
1999
+ const body = await req.json().catch(() => ({}));
2000
+ if (body.type === "direct" && body.otherUserId) {
2001
+ const conv = await createConversation(ctx.user.id, { type: "direct", otherUserId: body.otherUserId });
2002
+ return created(conv);
2003
+ }
2004
+ if (body.type === "group" && Array.isArray(body.memberIds)) {
2005
+ const conv = await createConversation(ctx.user.id, { type: "group", memberIds: body.memberIds });
2006
+ return created(conv);
2007
+ }
2008
+ return badRequest("conversation input invalid");
2009
+ });
2010
+ app.get(`${p}/conversations`, async (_req, ctx) => {
2011
+ if (!ctx.user) throw new HttpError("Unauthorized", 401);
2012
+ return ok(await listConversations(ctx.user.id));
2013
+ });
2014
+ app.get(`${p}/conversations/:id/messages`, async (req, ctx) => {
2015
+ if (!ctx.user) throw new HttpError("Unauthorized", 401);
2016
+ const convId = ctx.params.id;
2017
+ await requireMember(convId, ctx);
2018
+ const url = new URL(req.url);
2019
+ const before = url.searchParams.get("before") ?? void 0;
2020
+ const limit = Number(url.searchParams.get("limit") ?? 50);
2021
+ return ok(await listMessages(convId, { before, limit }));
2022
+ });
2023
+ app.post(`${p}/conversations/:id/messages`, async (req, ctx) => {
2024
+ if (!ctx.user) throw new HttpError("Unauthorized", 401);
2025
+ const convId = ctx.params.id;
2026
+ await requireMember(convId, ctx);
2027
+ const body = await req.json().catch(() => ({}));
2028
+ if (!body.content?.trim()) return badRequest("content is required");
2029
+ const message = await sendMessage(convId, {
2030
+ senderType: "user",
2031
+ senderId: ctx.user.id,
2032
+ content: body.content,
2033
+ msgType: body.msgType ?? "text"
2034
+ });
2035
+ broadcast(`conv:${convId}`, { type: "new_message", message });
2036
+ return created(message);
2037
+ });
2038
+ app.post(`${p}/conversations/:id/read`, async (_req, ctx) => {
2039
+ if (!ctx.user) throw new HttpError("Unauthorized", 401);
2040
+ const convId = ctx.params.id;
2041
+ await requireMember(convId, ctx);
2042
+ await markRead(convId, ctx.user.id);
2043
+ return noContent();
2044
+ });
2045
+ app.patch(`${p}/messages/:id`, async (req, ctx) => {
2046
+ if (!ctx.user) throw new HttpError("Unauthorized", 401);
2047
+ const messageId = ctx.params.id;
2048
+ const body = await req.json().catch(() => ({}));
2049
+ if (!body.content?.trim()) return badRequest("content is required");
2050
+ const edited = await editMessage(messageId, body.content);
2051
+ if (!edited) return badRequest("message not found");
2052
+ if (edited.sender_type === "user" && edited.sender_id !== ctx.user.id) {
2053
+ throw new HttpError("Forbidden: not your message", 403);
2054
+ }
2055
+ broadcast(`conv:${edited.conversation_id}`, { type: "message_edited", message: edited });
2056
+ return ok(edited);
2057
+ });
2058
+ app.delete(`${p}/messages/:id`, async (req, ctx) => {
2059
+ if (!ctx.user) throw new HttpError("Unauthorized", 401);
2060
+ const messageId = ctx.params.id;
2061
+ const list = await sql.unsafe(
2062
+ `SELECT conversation_id, sender_type, sender_id FROM ${MESSAGES} WHERE id = $1`,
2063
+ [messageId]
2064
+ );
2065
+ if (!list.length) return badRequest("message not found");
2066
+ const row = list[0];
2067
+ if (row.sender_type === "user" && row.sender_id !== ctx.user.id) {
2068
+ throw new HttpError("Forbidden: not your message", 403);
2069
+ }
2070
+ await requireMember(row.conversation_id, ctx);
2071
+ await deleteMessage(messageId);
2072
+ broadcast(`conv:${row.conversation_id}`, { type: "message_deleted", messageId });
2073
+ return noContent();
2074
+ });
2075
+ };
2076
+ return mw;
2077
+ }
2078
+
1666
2079
  // src/queue/index.ts
1667
2080
  import { randomUUID } from "node:crypto";
1668
2081
  import { hostname } from "node:os";
@@ -2674,9 +3087,56 @@ async function providerError(res) {
2674
3087
  else code = "provider_error";
2675
3088
  return { code, message: message || `provider error (${status})` };
2676
3089
  }
3090
+ function createEmbeddingClient(ebd) {
3091
+ const apiKey = ebd?.apiKey ?? process.env.DASHSCOPE_API_KEY ?? "";
3092
+ const configured = !!(ebd?.apiKey || process.env.DASHSCOPE_API_KEY);
3093
+ const baseUrl = ebd?.baseUrl ?? process.env.DASHSCOPE_BASE_URL ?? "https://dashscope.aliyuncs.com/compatible-mode/v1";
3094
+ const defaultModel = ebd?.defaultModel ?? process.env.DASHSCOPE_EMBEDDING_MODEL ?? "text-embedding-v4";
3095
+ const endpoint = `${baseUrl.replace(/\/$/, "")}/embeddings`;
3096
+ const headers = { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` };
3097
+ async function embedMany(texts) {
3098
+ if (!configured) {
3099
+ throw new AiError("unsupported", "ai embedding: \u672A\u914D\u7F6E\u2014\u2014\u4F20 ai({ embedding }) \u6216\u8BBE DASHSCOPE_API_KEY\uFF08DeepSeek \u65E0 embedding API\uFF0C\u9700\u72EC\u7ACB provider\uFF09");
3100
+ }
3101
+ if (!apiKey) {
3102
+ throw new AiError("auth_failed", "ai embedding: DASHSCOPE_API_KEY \u672A\u8BBE\u7F6E");
3103
+ }
3104
+ const controller = new AbortController();
3105
+ const timer = setTimeout(() => controller.abort(), 3e3);
3106
+ let res;
3107
+ try {
3108
+ res = await fetch(endpoint, {
3109
+ method: "POST",
3110
+ headers,
3111
+ body: JSON.stringify({ model: defaultModel, input: texts }),
3112
+ signal: controller.signal
3113
+ });
3114
+ } catch (err) {
3115
+ if (controller.signal.aborted) throw new AiError("provider_error", "embedding \u8BF7\u6C42\u8D85\u65F6\uFF083s\uFF09");
3116
+ throw new AiError("provider_error", err instanceof Error ? err.message : String(err));
3117
+ } finally {
3118
+ clearTimeout(timer);
3119
+ }
3120
+ if (!res.ok) {
3121
+ const { code, message } = await providerError(res);
3122
+ throw new AiError(code, message);
3123
+ }
3124
+ const data = await res.json();
3125
+ data.data.sort((a, b) => a.index - b.index);
3126
+ return data.data.map((item) => item.embedding);
3127
+ }
3128
+ return {
3129
+ async embed(text) {
3130
+ const results = await embedMany([text]);
3131
+ return results[0];
3132
+ },
3133
+ embedMany
3134
+ };
3135
+ }
2677
3136
  function createAiClient(opts) {
2678
3137
  const endpoint = `${opts.baseUrl.replace(/\/$/, "")}/chat/completions`;
2679
3138
  const headers = { "Content-Type": "application/json", Authorization: `Bearer ${opts.apiKey}` };
3139
+ const embedding = createEmbeddingClient(opts.embedding);
2680
3140
  const approvals = /* @__PURE__ */ new Map();
2681
3141
  function approve(response) {
2682
3142
  const resolve3 = approvals.get(response.id);
@@ -2810,7 +3270,17 @@ function createAiClient(opts) {
2810
3270
  }
2811
3271
  return sseResponse(run, { onAbort: () => controller.abort() });
2812
3272
  }
2813
- return { chat, stream, sse, streamStep, waitApproval, approve };
3273
+ return {
3274
+ chat,
3275
+ stream,
3276
+ sse,
3277
+ streamStep,
3278
+ waitApproval,
3279
+ approve,
3280
+ // embedding:未配置 provider 时明确抛 AiError(诚实裁剪:不静默降级)
3281
+ embed: (text) => embedding.embed(text),
3282
+ embedMany: (texts) => embedding.embedMany(texts)
3283
+ };
2814
3284
  }
2815
3285
  var DEFAULT_APPROVAL_TIMEOUT = 5 * 6e4;
2816
3286
  function safeParseArgs(raw) {
@@ -2830,13 +3300,23 @@ function createAgent(client, config) {
2830
3300
  type: "function",
2831
3301
  function: { name: t.name, description: t.description ?? "", parameters: t.parameters ?? {} }
2832
3302
  }));
2833
- function run(messages, options) {
3303
+ function createController(external) {
2834
3304
  const controller = new AbortController();
2835
- const external = options?.signal;
2836
3305
  if (external) {
2837
3306
  if (external.aborted) controller.abort();
2838
3307
  else external.addEventListener("abort", () => controller.abort(), { once: true });
2839
3308
  }
3309
+ return controller;
3310
+ }
3311
+ async function stream(messages, options) {
3312
+ const controller = createController(options?.signal);
3313
+ const emit = options?.emit ?? (() => {
3314
+ });
3315
+ emit("wf:message_start", { id: options?.traceId ?? randomUUID3() });
3316
+ await loop(messages, emit, controller.signal);
3317
+ }
3318
+ function run(messages, options) {
3319
+ const controller = createController(options?.signal);
2840
3320
  return sseResponse(
2841
3321
  async (emit) => {
2842
3322
  emit("wf:message_start", { id: options?.traceId ?? randomUUID3() });
@@ -2845,6 +3325,35 @@ function createAgent(client, config) {
2845
3325
  { onAbort: () => controller.abort() }
2846
3326
  );
2847
3327
  }
3328
+ async function runToResult(messages, options) {
3329
+ let content = "";
3330
+ const steps = [];
3331
+ let usage;
3332
+ const emit = (name, data) => {
3333
+ if (name === "wf:token") {
3334
+ content += data.text;
3335
+ } else if (name === "wf:step") {
3336
+ const s = data;
3337
+ if (s.type === "llm") steps.push({ type: "llm", content: s.content });
3338
+ else steps.push({ type: "tool_call", toolCall: { id: s.toolCallId ?? "", name: s.name ?? "", arguments: "" } });
3339
+ } else if (name === "wf:tool_result") {
3340
+ const r = data;
3341
+ const last = steps[steps.length - 1];
3342
+ if (last && last.type === "tool_call") {
3343
+ steps[steps.length - 1] = {
3344
+ type: "tool_result",
3345
+ toolCall: last.toolCall,
3346
+ toolResult: r.ok ? JSON.stringify(r.output ?? "") : `Error: ${r.error?.message ?? "unknown"}`
3347
+ };
3348
+ }
3349
+ } else if (name === "wf:usage") {
3350
+ usage = data;
3351
+ }
3352
+ };
3353
+ const controller = createController(options?.signal);
3354
+ await loop(messages, emit, controller.signal);
3355
+ return { content, steps, usage };
3356
+ }
2848
3357
  async function loop(messages, emit, signal) {
2849
3358
  const all = [{ role: "system", content: config.systemPrompt }, ...messages];
2850
3359
  const usage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
@@ -2920,7 +3429,7 @@ function createAgent(client, config) {
2920
3429
  emit("wf:usage", usage);
2921
3430
  emit("wf:done", { content: "", usage });
2922
3431
  }
2923
- return { run };
3432
+ return { run, stream, runToResult };
2924
3433
  }
2925
3434
 
2926
3435
  // src/ai/index.ts
@@ -2931,7 +3440,7 @@ function ai(options) {
2931
3440
  if (!apiKey) {
2932
3441
  throw new Error("ai: DEEPSEEK_API_KEY \u672A\u8BBE\u7F6E\u3002\u8BF7\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF\u6216\u4F20\u5165 apiKey");
2933
3442
  }
2934
- const client = createAiClient({ apiKey, baseUrl, defaultModel });
3443
+ const client = createAiClient({ apiKey, baseUrl, defaultModel, embedding: options?.embedding });
2935
3444
  const mw = (req, ctx, next) => {
2936
3445
  ctx.ai = module;
2937
3446
  return next(req, ctx);
@@ -2945,6 +3454,8 @@ function ai(options) {
2945
3454
  module.waitApproval = client.waitApproval;
2946
3455
  module.approve = client.approve;
2947
3456
  module.agent = (config) => createAgent(client, config);
3457
+ module.embed = client.embed;
3458
+ module.embedMany = client.embedMany;
2948
3459
  module.close = async () => {
2949
3460
  };
2950
3461
  return module;
@@ -4563,6 +5074,7 @@ export {
4563
5074
  created,
4564
5075
  email,
4565
5076
  forbidden,
5077
+ messager,
4566
5078
  noContent,
4567
5079
  notFound,
4568
5080
  ok,