@tuturuuu/ai 0.2.1 → 0.3.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.
- package/README.md +5 -0
- package/package.json +60 -60
- package/src/chat/google/route.ts +2 -3
- package/src/chat-sdk/registry.ts +2 -8
- package/src/chat-sdk/zalo-personal.ts +239 -10
- package/src/effect.ts +66 -0
- package/src/generate/route.ts +3 -6
- package/src/mind/route.ts +38 -15
- package/src/scheduling/duration-optimizer.ts +3 -6
- package/src/tools/executors/parallel-checks.ts +1 -1
- package/src/tools/executors/search.ts +2 -3
- package/src/tools/google-search-tool.ts +10 -0
package/README.md
CHANGED
|
@@ -61,6 +61,11 @@ yarn add @tuturuuu/ai
|
|
|
61
61
|
bun add @tuturuuu/ai
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
Published artifacts replace local `workspace:*` dependencies with exact
|
|
65
|
+
registry versions before packing. If a public consumer cannot install this
|
|
66
|
+
package, rerun the package release from the current production commit so its
|
|
67
|
+
manifest points at published Tuturuuu package versions.
|
|
68
|
+
|
|
64
69
|
## Development
|
|
65
70
|
|
|
66
71
|
```bash
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuturuuu/ai",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/tutur3u/platform",
|
|
@@ -19,96 +19,96 @@
|
|
|
19
19
|
],
|
|
20
20
|
"scripts": {
|
|
21
21
|
"test": "vitest run",
|
|
22
|
-
"type-check": "
|
|
22
|
+
"type-check": "tsc --project tsconfig.typecheck.json"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@ai-sdk/amazon-bedrock": "^
|
|
26
|
-
"@ai-sdk/anthropic": "^
|
|
27
|
-
"@ai-sdk/azure": "^
|
|
28
|
-
"@ai-sdk/cerebras": "^
|
|
29
|
-
"@ai-sdk/cohere": "^
|
|
30
|
-
"@ai-sdk/deepgram": "^
|
|
31
|
-
"@ai-sdk/deepinfra": "^
|
|
32
|
-
"@ai-sdk/deepseek": "^
|
|
33
|
-
"@ai-sdk/elevenlabs": "^
|
|
34
|
-
"@ai-sdk/fal": "^
|
|
35
|
-
"@ai-sdk/fireworks": "^
|
|
36
|
-
"@ai-sdk/gateway": "^
|
|
37
|
-
"@ai-sdk/gladia": "^
|
|
38
|
-
"@ai-sdk/google": "^
|
|
39
|
-
"@ai-sdk/google-vertex": "^
|
|
40
|
-
"@ai-sdk/groq": "^
|
|
41
|
-
"@ai-sdk/hume": "^
|
|
42
|
-
"@ai-sdk/lmnt": "^
|
|
43
|
-
"@ai-sdk/luma": "^
|
|
44
|
-
"@ai-sdk/mistral": "^
|
|
45
|
-
"@ai-sdk/openai": "^
|
|
46
|
-
"@ai-sdk/openai-compatible": "^
|
|
47
|
-
"@ai-sdk/perplexity": "^
|
|
48
|
-
"@ai-sdk/react": "^
|
|
49
|
-
"@ai-sdk/replicate": "^
|
|
50
|
-
"@ai-sdk/revai": "^
|
|
51
|
-
"@ai-sdk/togetherai": "^
|
|
52
|
-
"@ai-sdk/xai": "^
|
|
25
|
+
"@ai-sdk/amazon-bedrock": "^5.0.24",
|
|
26
|
+
"@ai-sdk/anthropic": "^4.0.16",
|
|
27
|
+
"@ai-sdk/azure": "^4.0.17",
|
|
28
|
+
"@ai-sdk/cerebras": "^3.0.12",
|
|
29
|
+
"@ai-sdk/cohere": "^4.0.11",
|
|
30
|
+
"@ai-sdk/deepgram": "^3.0.11",
|
|
31
|
+
"@ai-sdk/deepinfra": "^3.0.12",
|
|
32
|
+
"@ai-sdk/deepseek": "^3.0.12",
|
|
33
|
+
"@ai-sdk/elevenlabs": "^3.0.12",
|
|
34
|
+
"@ai-sdk/fal": "^3.0.12",
|
|
35
|
+
"@ai-sdk/fireworks": "^3.0.13",
|
|
36
|
+
"@ai-sdk/gateway": "^4.0.23",
|
|
37
|
+
"@ai-sdk/gladia": "^3.0.11",
|
|
38
|
+
"@ai-sdk/google": "^4.0.18",
|
|
39
|
+
"@ai-sdk/google-vertex": "^5.0.22",
|
|
40
|
+
"@ai-sdk/groq": "^4.0.12",
|
|
41
|
+
"@ai-sdk/hume": "^3.0.11",
|
|
42
|
+
"@ai-sdk/lmnt": "^3.0.11",
|
|
43
|
+
"@ai-sdk/luma": "^3.0.12",
|
|
44
|
+
"@ai-sdk/mistral": "^4.0.13",
|
|
45
|
+
"@ai-sdk/openai": "^4.0.16",
|
|
46
|
+
"@ai-sdk/openai-compatible": "^3.0.12",
|
|
47
|
+
"@ai-sdk/perplexity": "^4.0.12",
|
|
48
|
+
"@ai-sdk/react": "^4.0.34",
|
|
49
|
+
"@ai-sdk/replicate": "^3.0.12",
|
|
50
|
+
"@ai-sdk/revai": "^3.0.11",
|
|
51
|
+
"@ai-sdk/togetherai": "^3.0.13",
|
|
52
|
+
"@ai-sdk/xai": "^4.0.16",
|
|
53
53
|
"@beeper/chat-adapter-matrix": "^0.2.0",
|
|
54
54
|
"@bitbasti/chat-adapter-webex": "^0.1.0",
|
|
55
|
-
"@chat-adapter/discord": "^4.
|
|
56
|
-
"@chat-adapter/gchat": "^4.
|
|
57
|
-
"@chat-adapter/github": "^4.
|
|
58
|
-
"@chat-adapter/linear": "^4.
|
|
59
|
-
"@chat-adapter/messenger": "^4.
|
|
60
|
-
"@chat-adapter/slack": "^4.
|
|
61
|
-
"@chat-adapter/state-ioredis": "^4.
|
|
62
|
-
"@chat-adapter/state-memory": "^4.
|
|
63
|
-
"@chat-adapter/state-pg": "^4.
|
|
64
|
-
"@chat-adapter/state-redis": "^4.
|
|
65
|
-
"@chat-adapter/teams": "^4.
|
|
66
|
-
"@chat-adapter/telegram": "^4.
|
|
67
|
-
"@chat-adapter/web": "^4.
|
|
68
|
-
"@chat-adapter/whatsapp": "^4.
|
|
55
|
+
"@chat-adapter/discord": "^4.34.0",
|
|
56
|
+
"@chat-adapter/gchat": "^4.34.0",
|
|
57
|
+
"@chat-adapter/github": "^4.34.0",
|
|
58
|
+
"@chat-adapter/linear": "^4.34.0",
|
|
59
|
+
"@chat-adapter/messenger": "^4.34.0",
|
|
60
|
+
"@chat-adapter/slack": "^4.34.0",
|
|
61
|
+
"@chat-adapter/state-ioredis": "^4.34.0",
|
|
62
|
+
"@chat-adapter/state-memory": "^4.34.0",
|
|
63
|
+
"@chat-adapter/state-pg": "^4.34.0",
|
|
64
|
+
"@chat-adapter/state-redis": "^4.34.0",
|
|
65
|
+
"@chat-adapter/teams": "^4.34.0",
|
|
66
|
+
"@chat-adapter/telegram": "^4.34.0",
|
|
67
|
+
"@chat-adapter/web": "^4.34.0",
|
|
68
|
+
"@chat-adapter/whatsapp": "^4.34.0",
|
|
69
69
|
"@json-render/core": "^0.19.0",
|
|
70
70
|
"@json-render/react": "^0.19.0",
|
|
71
|
-
"@liveblocks/chat-sdk-adapter": "^3.
|
|
71
|
+
"@liveblocks/chat-sdk-adapter": "^3.22.0",
|
|
72
72
|
"@octokit/rest": "^22.0.1",
|
|
73
73
|
"@resend/chat-sdk-adapter": "^0.2.2",
|
|
74
74
|
"@streamdown/cjk": "^1.0.3",
|
|
75
75
|
"@streamdown/code": "^1.1.1",
|
|
76
76
|
"@streamdown/math": "^1.0.2",
|
|
77
77
|
"@streamdown/mermaid": "^1.0.2",
|
|
78
|
-
"@tuturuuu/google": "0.0.
|
|
79
|
-
"@tuturuuu/internal-api": "0.
|
|
80
|
-
"@tuturuuu/supabase": "0.
|
|
81
|
-
"@tuturuuu/utils": "0.
|
|
82
|
-
"@vercel/sandbox": "^2.
|
|
83
|
-
"@zernio/chat-sdk-adapter": "^0.
|
|
84
|
-
"ai": "^
|
|
85
|
-
"bash-tool": "^1.3.
|
|
86
|
-
"chat": "^4.
|
|
78
|
+
"@tuturuuu/google": "0.0.3",
|
|
79
|
+
"@tuturuuu/internal-api": "0.20.0",
|
|
80
|
+
"@tuturuuu/supabase": "0.4.1",
|
|
81
|
+
"@tuturuuu/utils": "0.17.0",
|
|
82
|
+
"@vercel/sandbox": "^2.7.1",
|
|
83
|
+
"@zernio/chat-sdk-adapter": "^0.4.0",
|
|
84
|
+
"ai": "^7.0.31",
|
|
85
|
+
"bash-tool": "^1.3.18",
|
|
86
|
+
"chat": "^4.34.0",
|
|
87
87
|
"chat-adapter-baileys": "^2.0.3",
|
|
88
88
|
"chat-adapter-blooio": "^0.1.0",
|
|
89
|
-
"chat-adapter-imessage": "^
|
|
89
|
+
"chat-adapter-imessage": "^1.1.0",
|
|
90
90
|
"chat-adapter-mattermost": "^1.1.3",
|
|
91
91
|
"chat-adapter-sendblue": "^0.2.0",
|
|
92
92
|
"chat-adapter-zalo": "^0.1.0",
|
|
93
93
|
"chat-state-cloudflare-do": "^0.2.0",
|
|
94
94
|
"chat-state-mysql": "^0.1.0",
|
|
95
95
|
"dayjs": "^1.11.20",
|
|
96
|
-
"next": "^16.
|
|
96
|
+
"next": "^16.3.0-preview.6",
|
|
97
97
|
"qrcode": "^1.5.4",
|
|
98
98
|
"react": "^19.2.7",
|
|
99
99
|
"react-dom": "^19.2.7",
|
|
100
100
|
"streamdown": "^2.5.0",
|
|
101
|
-
"uuid": "^14.0.
|
|
101
|
+
"uuid": "^14.0.1",
|
|
102
102
|
"zca-js": "^2.1.2",
|
|
103
103
|
"zod": "^4.4.3"
|
|
104
104
|
},
|
|
105
105
|
"devDependencies": {
|
|
106
|
-
"@tuturuuu/types": "0.
|
|
106
|
+
"@tuturuuu/types": "0.19.0",
|
|
107
107
|
"@tuturuuu/typescript-config": "0.1.1",
|
|
108
|
-
"@types/node": "^
|
|
108
|
+
"@types/node": "^26.1.1",
|
|
109
109
|
"@types/qrcode": "^1.5.6",
|
|
110
110
|
"@types/react": "^19.2.17",
|
|
111
|
-
"typescript": "
|
|
111
|
+
"typescript": "7.0.2"
|
|
112
112
|
},
|
|
113
113
|
"exports": {
|
|
114
114
|
"./*": "./src/*.ts"
|
package/src/chat/google/route.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
resolvePlanModel,
|
|
19
19
|
} from '../../credits/resolve-plan-model';
|
|
20
20
|
import { withAiMemory } from '../../memory';
|
|
21
|
+
import { createGoogleSearchToolSet } from '../../tools/google-search-tool';
|
|
21
22
|
import type { CreditSource as SharedCreditSource } from '../credit-source';
|
|
22
23
|
import {
|
|
23
24
|
shouldForceGoogleSearchForLatestUserMessage,
|
|
@@ -495,9 +496,7 @@ export function createPOST(
|
|
|
495
496
|
);
|
|
496
497
|
|
|
497
498
|
// Provider-native Google Search tool for non-Mira mode.
|
|
498
|
-
const googleSearchTool =
|
|
499
|
-
google_search: google.tools.googleSearch({}),
|
|
500
|
-
};
|
|
499
|
+
const googleSearchTool = createGoogleSearchToolSet();
|
|
501
500
|
|
|
502
501
|
type PrepareStep = NonNullable<
|
|
503
502
|
NonNullable<Parameters<typeof streamText>[0]>['prepareStep']
|
package/src/chat-sdk/registry.ts
CHANGED
|
@@ -83,18 +83,12 @@ async function loadChatSdkFactory(
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
function isAdapter(value: unknown): value is Adapter {
|
|
86
|
-
return
|
|
87
|
-
typeof value === 'object' &&
|
|
88
|
-
value !== null &&
|
|
89
|
-
'handleWebhook' in value &&
|
|
90
|
-
'name' in value
|
|
91
|
-
);
|
|
86
|
+
return value instanceof Object && 'handleWebhook' in value && 'name' in value;
|
|
92
87
|
}
|
|
93
88
|
|
|
94
89
|
function isStateAdapter(value: unknown): value is StateAdapter {
|
|
95
90
|
return (
|
|
96
|
-
|
|
97
|
-
value !== null &&
|
|
91
|
+
value instanceof Object &&
|
|
98
92
|
'get' in value &&
|
|
99
93
|
'set' in value &&
|
|
100
94
|
'delete' in value
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type Adapter,
|
|
3
3
|
type AdapterPostableMessage,
|
|
4
|
+
type Attachment,
|
|
4
5
|
type ChatInstance,
|
|
5
6
|
type FetchOptions,
|
|
6
7
|
type FetchResult,
|
|
@@ -163,6 +164,7 @@ export interface ZaloPersonalPhoneSyncOptions {
|
|
|
163
164
|
maxPulls?: number;
|
|
164
165
|
minSeqId?: number;
|
|
165
166
|
pullDelayMs?: number;
|
|
167
|
+
signal?: AbortSignal;
|
|
166
168
|
tempKey?: string;
|
|
167
169
|
useListenerWakeup?: boolean;
|
|
168
170
|
}
|
|
@@ -211,8 +213,8 @@ const DEFAULT_HISTORY_MAX_PAGES_PER_TYPE = 1000;
|
|
|
211
213
|
const DEFAULT_HISTORY_MESSAGES_PER_GROUP = 200;
|
|
212
214
|
const DEFAULT_HISTORY_PAGE_TIMEOUT_MS = 8000;
|
|
213
215
|
const HISTORY_CONNECT_TIMEOUT_MS = 5000;
|
|
214
|
-
const DEFAULT_PHONE_SYNC_MAX_PULLS =
|
|
215
|
-
const DEFAULT_PHONE_SYNC_PULL_DELAY_MS =
|
|
216
|
+
const DEFAULT_PHONE_SYNC_MAX_PULLS = 4;
|
|
217
|
+
const DEFAULT_PHONE_SYNC_PULL_DELAY_MS = 20_000;
|
|
216
218
|
const PHONE_SYNC_PC_NAME_MAX_LENGTH = 80;
|
|
217
219
|
const ZALO_LISTENER_SOCKET_OPEN_STATE = 1;
|
|
218
220
|
const ZALO_PHONE_SYNC_REQUEST_CMD = 590;
|
|
@@ -1023,15 +1025,16 @@ export function createZaloPersonalAdapter(
|
|
|
1023
1025
|
});
|
|
1024
1026
|
}
|
|
1025
1027
|
|
|
1028
|
+
const attachments = extractZaloPersonalAttachments(raw, config);
|
|
1026
1029
|
const text =
|
|
1027
1030
|
typeof raw.data.content === 'string'
|
|
1028
1031
|
? raw.data.content
|
|
1029
|
-
:
|
|
1032
|
+
: zaloAttachmentFallbackText(raw.data.msgType, attachments);
|
|
1030
1033
|
const authorId =
|
|
1031
1034
|
raw.data.uidFrom || raw.data.userId || (raw.isSelf ? status.ownId : '');
|
|
1032
1035
|
|
|
1033
1036
|
return new Message<ZaloPersonalRawMessage>({
|
|
1034
|
-
attachments
|
|
1037
|
+
attachments,
|
|
1035
1038
|
author: {
|
|
1036
1039
|
fullName: raw.data.dName || authorId || 'Zalo user',
|
|
1037
1040
|
isBot: raw.isSelf,
|
|
@@ -1245,6 +1248,8 @@ export function createZaloPersonalAdapter(
|
|
|
1245
1248
|
const useListenerWakeup = options.useListenerWakeup ?? true;
|
|
1246
1249
|
|
|
1247
1250
|
try {
|
|
1251
|
+
assertPhoneSyncNotAborted(options.signal);
|
|
1252
|
+
|
|
1248
1253
|
if (useListenerWakeup) {
|
|
1249
1254
|
attachListeners(apiInstance);
|
|
1250
1255
|
await adapter.startPersonalListener();
|
|
@@ -1276,9 +1281,8 @@ export function createZaloPersonalAdapter(
|
|
|
1276
1281
|
}
|
|
1277
1282
|
|
|
1278
1283
|
for (let attempt = 0; attempt < maxPulls; attempt += 1) {
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
}
|
|
1284
|
+
await delay(pullDelayMs, options.signal);
|
|
1285
|
+
assertPhoneSyncNotAborted(options.signal);
|
|
1282
1286
|
|
|
1283
1287
|
try {
|
|
1284
1288
|
const pullResponse = await transferApi.tuturuuuPullMobileMessages?.(
|
|
@@ -1332,7 +1336,11 @@ export function createZaloPersonalAdapter(
|
|
|
1332
1336
|
}
|
|
1333
1337
|
}
|
|
1334
1338
|
} catch (error) {
|
|
1335
|
-
lastError =
|
|
1339
|
+
lastError = options.signal?.aborted
|
|
1340
|
+
? 'zalo_personal_phone_sync_cancelled'
|
|
1341
|
+
: error instanceof Error
|
|
1342
|
+
? error.message
|
|
1343
|
+
: String(error);
|
|
1336
1344
|
} finally {
|
|
1337
1345
|
if (requestAccepted && messages.length > 0) {
|
|
1338
1346
|
cleaned =
|
|
@@ -2124,8 +2132,229 @@ function stringValue(value: unknown) {
|
|
|
2124
2132
|
return '';
|
|
2125
2133
|
}
|
|
2126
2134
|
|
|
2127
|
-
|
|
2128
|
-
|
|
2135
|
+
const ZALO_MEDIA_URL_KEYS = [
|
|
2136
|
+
'downloadUrl',
|
|
2137
|
+
'fileUrl',
|
|
2138
|
+
'hdUrl',
|
|
2139
|
+
'href',
|
|
2140
|
+
'normalUrl',
|
|
2141
|
+
'originUrl',
|
|
2142
|
+
'thumb',
|
|
2143
|
+
'thumbnail',
|
|
2144
|
+
'thumbUrl',
|
|
2145
|
+
'url',
|
|
2146
|
+
'voiceUrl',
|
|
2147
|
+
] as const;
|
|
2148
|
+
|
|
2149
|
+
function extractZaloPersonalAttachments(
|
|
2150
|
+
raw: ZcaMessage,
|
|
2151
|
+
config: ZaloPersonalAdapterConfig
|
|
2152
|
+
): Attachment[] {
|
|
2153
|
+
const content = normalizeZaloAttachmentContent(raw.data.content);
|
|
2154
|
+
if (!content) return [];
|
|
2155
|
+
|
|
2156
|
+
const urls = collectZaloMediaUrls(content);
|
|
2157
|
+
if (urls.length === 0) return [];
|
|
2158
|
+
|
|
2159
|
+
const type = zaloAttachmentType(raw.data.msgType);
|
|
2160
|
+
const preferredUrl = pickPreferredZaloMediaUrl(urls);
|
|
2161
|
+
const name = zaloAttachmentFilename(content, preferredUrl, type);
|
|
2162
|
+
const mimeType = inferZaloAttachmentMimeType(name, type);
|
|
2163
|
+
const size = numericValue(
|
|
2164
|
+
content.fileSize ?? content.file_size ?? content.size ?? content.totalSize
|
|
2165
|
+
);
|
|
2166
|
+
|
|
2167
|
+
return [
|
|
2168
|
+
{
|
|
2169
|
+
fetchData: async () => {
|
|
2170
|
+
const response = await fetch(preferredUrl, {
|
|
2171
|
+
headers: {
|
|
2172
|
+
Cookie: zaloCookieHeader(config.cookieJson),
|
|
2173
|
+
'User-Agent': config.userAgent,
|
|
2174
|
+
},
|
|
2175
|
+
signal: AbortSignal.timeout(30_000),
|
|
2176
|
+
});
|
|
2177
|
+
if (!response.ok) {
|
|
2178
|
+
throw new Error(
|
|
2179
|
+
`zalo_personal_media_fetch_failed_${response.status}`
|
|
2180
|
+
);
|
|
2181
|
+
}
|
|
2182
|
+
const declaredSize = Number(response.headers.get('content-length'));
|
|
2183
|
+
if (Number.isFinite(declaredSize) && declaredSize > 25 * 1024 * 1024) {
|
|
2184
|
+
throw new Error('zalo_personal_media_too_large');
|
|
2185
|
+
}
|
|
2186
|
+
return Buffer.from(await response.arrayBuffer());
|
|
2187
|
+
},
|
|
2188
|
+
height: numericValue(content.height),
|
|
2189
|
+
mimeType,
|
|
2190
|
+
name,
|
|
2191
|
+
size,
|
|
2192
|
+
type,
|
|
2193
|
+
url: preferredUrl,
|
|
2194
|
+
width: numericValue(content.width),
|
|
2195
|
+
},
|
|
2196
|
+
];
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
function normalizeZaloAttachmentContent(value: unknown) {
|
|
2200
|
+
if (isRecord(value)) return value;
|
|
2201
|
+
if (typeof value !== 'string') return null;
|
|
2202
|
+
|
|
2203
|
+
const trimmed = value.trim();
|
|
2204
|
+
if (!trimmed.startsWith('{')) return null;
|
|
2205
|
+
|
|
2206
|
+
try {
|
|
2207
|
+
const parsed = JSON.parse(trimmed);
|
|
2208
|
+
return isRecord(parsed) ? parsed : null;
|
|
2209
|
+
} catch {
|
|
2210
|
+
return null;
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
function collectZaloMediaUrls(value: unknown) {
|
|
2215
|
+
const urls: string[] = [];
|
|
2216
|
+
|
|
2217
|
+
visitRecordValues(value, (key, candidate) => {
|
|
2218
|
+
if (
|
|
2219
|
+
ZALO_MEDIA_URL_KEYS.includes(
|
|
2220
|
+
key as (typeof ZALO_MEDIA_URL_KEYS)[number]
|
|
2221
|
+
) &&
|
|
2222
|
+
typeof candidate === 'string' &&
|
|
2223
|
+
/^https?:\/\//u.test(candidate)
|
|
2224
|
+
) {
|
|
2225
|
+
urls.push(candidate);
|
|
2226
|
+
}
|
|
2227
|
+
});
|
|
2228
|
+
|
|
2229
|
+
return [...new Set(urls)];
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
function pickPreferredZaloMediaUrl(urls: string[]) {
|
|
2233
|
+
return (
|
|
2234
|
+
urls.find((url) => /(?:hd|origin|download|file)/iu.test(url)) ?? urls[0]!
|
|
2235
|
+
);
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
function zaloAttachmentType(msgType: string): Attachment['type'] {
|
|
2239
|
+
if (/voice|audio/iu.test(msgType)) return 'audio';
|
|
2240
|
+
if (/video/iu.test(msgType)) return 'video';
|
|
2241
|
+
if (/photo|image|sticker|gif|doodle/iu.test(msgType)) return 'image';
|
|
2242
|
+
return 'file';
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
function zaloAttachmentFilename(
|
|
2246
|
+
content: Record<string, unknown>,
|
|
2247
|
+
url: string,
|
|
2248
|
+
type: Attachment['type']
|
|
2249
|
+
) {
|
|
2250
|
+
const explicit =
|
|
2251
|
+
stringValue(content.fileName) ||
|
|
2252
|
+
stringValue(content.filename) ||
|
|
2253
|
+
stringValue(content.title);
|
|
2254
|
+
if (explicit) return explicit.slice(0, 240);
|
|
2255
|
+
|
|
2256
|
+
try {
|
|
2257
|
+
const pathname = new URL(url).pathname;
|
|
2258
|
+
const candidate = decodeURIComponent(pathname.split('/').at(-1) ?? '');
|
|
2259
|
+
if (candidate?.includes('.')) return candidate.slice(0, 240);
|
|
2260
|
+
} catch {
|
|
2261
|
+
// Fall through to a stable generic name.
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
const extension =
|
|
2265
|
+
type === 'image'
|
|
2266
|
+
? 'jpg'
|
|
2267
|
+
: type === 'video'
|
|
2268
|
+
? 'mp4'
|
|
2269
|
+
: type === 'audio'
|
|
2270
|
+
? 'm4a'
|
|
2271
|
+
: 'bin';
|
|
2272
|
+
return `zalo-${type}.${extension}`;
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
function inferZaloAttachmentMimeType(name: string, type: Attachment['type']) {
|
|
2276
|
+
const extension = name.split('.').at(-1)?.toLowerCase();
|
|
2277
|
+
const byExtension: Record<string, string> = {
|
|
2278
|
+
gif: 'image/gif',
|
|
2279
|
+
jpeg: 'image/jpeg',
|
|
2280
|
+
jpg: 'image/jpeg',
|
|
2281
|
+
m4a: 'audio/mp4',
|
|
2282
|
+
mov: 'video/quicktime',
|
|
2283
|
+
mp3: 'audio/mpeg',
|
|
2284
|
+
mp4: 'video/mp4',
|
|
2285
|
+
ogg: 'audio/ogg',
|
|
2286
|
+
pdf: 'application/pdf',
|
|
2287
|
+
png: 'image/png',
|
|
2288
|
+
webm: 'video/webm',
|
|
2289
|
+
webp: 'image/webp',
|
|
2290
|
+
};
|
|
2291
|
+
|
|
2292
|
+
return (
|
|
2293
|
+
(extension && byExtension[extension]) ||
|
|
2294
|
+
(type === 'file' ? 'application/octet-stream' : `${type}/*`)
|
|
2295
|
+
);
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
function zaloAttachmentFallbackText(
|
|
2299
|
+
msgType: string,
|
|
2300
|
+
attachments: Attachment[]
|
|
2301
|
+
) {
|
|
2302
|
+
if (attachments.length === 0) return '[Unsupported Zalo message]';
|
|
2303
|
+
const type = zaloAttachmentType(msgType);
|
|
2304
|
+
return `[Zalo ${type}]`;
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
function numericValue(value: unknown) {
|
|
2308
|
+
const numeric =
|
|
2309
|
+
typeof value === 'number'
|
|
2310
|
+
? value
|
|
2311
|
+
: typeof value === 'string'
|
|
2312
|
+
? Number.parseInt(value, 10)
|
|
2313
|
+
: Number.NaN;
|
|
2314
|
+
return Number.isFinite(numeric) && numeric > 0 ? numeric : undefined;
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
function zaloCookieHeader(cookieJson: string) {
|
|
2318
|
+
try {
|
|
2319
|
+
const parsed = JSON.parse(cookieJson) as unknown;
|
|
2320
|
+
if (!Array.isArray(parsed)) return '';
|
|
2321
|
+
return parsed
|
|
2322
|
+
.filter(isRecord)
|
|
2323
|
+
.map((cookie) => {
|
|
2324
|
+
const name = stringValue(cookie.name);
|
|
2325
|
+
const value = stringValue(cookie.value);
|
|
2326
|
+
return name && value ? `${name}=${value}` : '';
|
|
2327
|
+
})
|
|
2328
|
+
.filter(Boolean)
|
|
2329
|
+
.join('; ');
|
|
2330
|
+
} catch {
|
|
2331
|
+
return '';
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
function delay(ms: number, signal?: AbortSignal) {
|
|
2336
|
+
if (signal?.aborted) {
|
|
2337
|
+
return Promise.reject(new Error('zalo_personal_phone_sync_cancelled'));
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
return new Promise<void>((resolve, reject) => {
|
|
2341
|
+
const timer = setTimeout(() => {
|
|
2342
|
+
signal?.removeEventListener('abort', onAbort);
|
|
2343
|
+
resolve();
|
|
2344
|
+
}, ms);
|
|
2345
|
+
const onAbort = () => {
|
|
2346
|
+
clearTimeout(timer);
|
|
2347
|
+
reject(new Error('zalo_personal_phone_sync_cancelled'));
|
|
2348
|
+
};
|
|
2349
|
+
|
|
2350
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
2351
|
+
});
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
function assertPhoneSyncNotAborted(signal?: AbortSignal) {
|
|
2355
|
+
if (signal?.aborted) {
|
|
2356
|
+
throw new Error('zalo_personal_phone_sync_cancelled');
|
|
2357
|
+
}
|
|
2129
2358
|
}
|
|
2130
2359
|
|
|
2131
2360
|
function dateFromZaloTimestamp(value: string) {
|
package/src/effect.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CreditCheckResult,
|
|
3
|
+
CreditDeductionResult,
|
|
4
|
+
DeductCreditsParams,
|
|
5
|
+
} from '@tuturuuu/ai/credits/types';
|
|
6
|
+
import {
|
|
7
|
+
Context,
|
|
8
|
+
Effect,
|
|
9
|
+
fromPromise,
|
|
10
|
+
Layer,
|
|
11
|
+
type TuturuuuEffectError,
|
|
12
|
+
} from '@tuturuuu/utils/effect';
|
|
13
|
+
import { checkAiCredits, deductAiCredits } from './credits/check-credits';
|
|
14
|
+
import type { AiFeature } from './credits/constants';
|
|
15
|
+
|
|
16
|
+
export interface AiCreditsServiceShape {
|
|
17
|
+
readonly checkAiCredits: typeof checkAiCredits;
|
|
18
|
+
readonly deductAiCredits: typeof deductAiCredits;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class AiCreditsService extends Context.Tag(
|
|
22
|
+
'@tuturuuu/ai/AiCreditsService'
|
|
23
|
+
)<AiCreditsService, AiCreditsServiceShape>() {}
|
|
24
|
+
|
|
25
|
+
export const AiCreditsLive = Layer.succeed(AiCreditsService, {
|
|
26
|
+
checkAiCredits,
|
|
27
|
+
deductAiCredits,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export function checkAiCreditsEffect(
|
|
31
|
+
wsId: string | undefined,
|
|
32
|
+
modelId: string,
|
|
33
|
+
feature: AiFeature,
|
|
34
|
+
opts?: { userId?: string; estimatedInputTokens?: number }
|
|
35
|
+
): Effect.Effect<CreditCheckResult, TuturuuuEffectError, AiCreditsService> {
|
|
36
|
+
return Effect.gen(function* () {
|
|
37
|
+
const service = yield* AiCreditsService;
|
|
38
|
+
|
|
39
|
+
return yield* fromPromise(
|
|
40
|
+
() => service.checkAiCredits(wsId, modelId, feature, opts),
|
|
41
|
+
{
|
|
42
|
+
code: 'AI_CREDIT_CHECK_FAILED',
|
|
43
|
+
message: 'AI credit check failed.',
|
|
44
|
+
context: { feature, modelId, wsId },
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function deductAiCreditsEffect(
|
|
51
|
+
params: DeductCreditsParams
|
|
52
|
+
): Effect.Effect<CreditDeductionResult, TuturuuuEffectError, AiCreditsService> {
|
|
53
|
+
return Effect.gen(function* () {
|
|
54
|
+
const service = yield* AiCreditsService;
|
|
55
|
+
|
|
56
|
+
return yield* fromPromise(() => service.deductAiCredits(params), {
|
|
57
|
+
code: 'AI_CREDIT_DEDUCTION_FAILED',
|
|
58
|
+
message: 'AI credit deduction failed.',
|
|
59
|
+
context: {
|
|
60
|
+
feature: params.feature,
|
|
61
|
+
modelId: params.modelId,
|
|
62
|
+
wsId: params.wsId,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
package/src/generate/route.ts
CHANGED
|
@@ -259,7 +259,7 @@ export function createPOST(
|
|
|
259
259
|
usage: {
|
|
260
260
|
inputTokens: usage.inputTokens ?? 0,
|
|
261
261
|
outputTokens: usage.outputTokens ?? 0,
|
|
262
|
-
reasoningTokens: usage.reasoningTokens ?? 0,
|
|
262
|
+
reasoningTokens: usage.outputTokenDetails?.reasoningTokens ?? 0,
|
|
263
263
|
totalTokens: usage.totalTokens ?? 0,
|
|
264
264
|
},
|
|
265
265
|
finishReason,
|
|
@@ -274,7 +274,7 @@ export function createPOST(
|
|
|
274
274
|
finish_reason: String(finishReason),
|
|
275
275
|
input_tokens: usage.inputTokens ?? 0,
|
|
276
276
|
output_tokens: usage.outputTokens ?? 0,
|
|
277
|
-
reasoning_tokens: usage.reasoningTokens ?? 0,
|
|
277
|
+
reasoning_tokens: usage.outputTokenDetails?.reasoningTokens ?? 0,
|
|
278
278
|
total_tokens: usage.totalTokens ?? 0,
|
|
279
279
|
system_prompt: configs.systemPrompt ?? '',
|
|
280
280
|
};
|
|
@@ -296,10 +296,7 @@ export function createPOST(
|
|
|
296
296
|
modelId: effectiveModel,
|
|
297
297
|
inputTokens: usage.inputTokens ?? 0,
|
|
298
298
|
outputTokens: usage.outputTokens ?? 0,
|
|
299
|
-
reasoningTokens:
|
|
300
|
-
usage.outputTokenDetails?.reasoningTokens ??
|
|
301
|
-
usage.reasoningTokens ??
|
|
302
|
-
0,
|
|
299
|
+
reasoningTokens: usage.outputTokenDetails?.reasoningTokens ?? 0,
|
|
303
300
|
feature: 'generate',
|
|
304
301
|
executionId: execData?.id,
|
|
305
302
|
}).catch((err) => console.error('Failed to deduct AI credits:', err));
|
package/src/mind/route.ts
CHANGED
|
@@ -5,7 +5,13 @@ import {
|
|
|
5
5
|
normalizeWorkspaceId,
|
|
6
6
|
verifyWorkspaceMembershipType,
|
|
7
7
|
} from '@tuturuuu/utils/workspace-helper';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
consumeStream,
|
|
10
|
+
gateway,
|
|
11
|
+
type LanguageModelUsage,
|
|
12
|
+
stepCountIs,
|
|
13
|
+
streamText,
|
|
14
|
+
} from 'ai';
|
|
9
15
|
import { type NextRequest, NextResponse } from 'next/server';
|
|
10
16
|
import { z } from 'zod';
|
|
11
17
|
import type { CreditSource as SharedCreditSource } from '../chat/credit-source';
|
|
@@ -31,6 +37,11 @@ import { createMiraStreamTools } from '../tools/mira-tools';
|
|
|
31
37
|
import { createMindStreamTools, type MindToolCallbacks } from './tools';
|
|
32
38
|
|
|
33
39
|
type AuthOk = Extract<AiRouteAuthResult, { ok: true }>;
|
|
40
|
+
type MindUsage = Partial<
|
|
41
|
+
Pick<LanguageModelUsage, 'inputTokens' | 'outputTokens' | 'totalTokens'>
|
|
42
|
+
> & {
|
|
43
|
+
outputTokenDetails?: Partial<LanguageModelUsage['outputTokenDetails']>;
|
|
44
|
+
};
|
|
34
45
|
|
|
35
46
|
const MindChatBodySchema = z.object({
|
|
36
47
|
boardId: z.guid().nullable().optional(),
|
|
@@ -589,22 +600,34 @@ export function createPOST(callbacks: MindRouteCallbacks) {
|
|
|
589
600
|
userId: auth.user.id,
|
|
590
601
|
wsId: access.wsId,
|
|
591
602
|
});
|
|
603
|
+
const telemetryRuntimeContext = {
|
|
604
|
+
boardId: parsedBody.boardId ?? 'none',
|
|
605
|
+
clientRunId: parsedBody.clientRunId ?? 'none',
|
|
606
|
+
creditSource: requestedCreditSource,
|
|
607
|
+
creditWsId: billingWsId ?? access.wsId,
|
|
608
|
+
model: resolvedModelId,
|
|
609
|
+
threadId,
|
|
610
|
+
thinkingMode: parsedBody.thinkingMode ?? 'fast',
|
|
611
|
+
writeMode,
|
|
612
|
+
wsId: access.wsId,
|
|
613
|
+
};
|
|
592
614
|
|
|
593
615
|
const result = streamText({
|
|
594
616
|
abortSignal: request.signal,
|
|
595
|
-
|
|
617
|
+
runtimeContext: telemetryRuntimeContext,
|
|
618
|
+
telemetry: {
|
|
596
619
|
functionId: 'mind.ai.stream',
|
|
597
620
|
isEnabled: true,
|
|
598
|
-
|
|
599
|
-
boardId:
|
|
600
|
-
clientRunId:
|
|
601
|
-
creditSource:
|
|
602
|
-
creditWsId:
|
|
603
|
-
model:
|
|
604
|
-
threadId,
|
|
605
|
-
thinkingMode:
|
|
606
|
-
writeMode,
|
|
607
|
-
wsId:
|
|
621
|
+
includeRuntimeContext: {
|
|
622
|
+
boardId: true,
|
|
623
|
+
clientRunId: true,
|
|
624
|
+
creditSource: true,
|
|
625
|
+
creditWsId: true,
|
|
626
|
+
model: true,
|
|
627
|
+
threadId: true,
|
|
628
|
+
thinkingMode: true,
|
|
629
|
+
writeMode: true,
|
|
630
|
+
wsId: true,
|
|
608
631
|
},
|
|
609
632
|
},
|
|
610
633
|
maxRetries: 0,
|
|
@@ -636,7 +659,7 @@ export function createPOST(callbacks: MindRouteCallbacks) {
|
|
|
636
659
|
const toolResults = response.steps?.flatMap(
|
|
637
660
|
(step) => step.toolResults ?? []
|
|
638
661
|
);
|
|
639
|
-
const usage = response.totalUsage ?? response.usage ?? {};
|
|
662
|
+
const usage: MindUsage = response.totalUsage ?? response.usage ?? {};
|
|
640
663
|
await callbacks.persistMessage({
|
|
641
664
|
boardId: parsedBody.boardId ?? null,
|
|
642
665
|
content: response.text || '',
|
|
@@ -653,7 +676,7 @@ export function createPOST(callbacks: MindRouteCallbacks) {
|
|
|
653
676
|
usage: {
|
|
654
677
|
inputTokens: usage.inputTokens ?? 0,
|
|
655
678
|
outputTokens: usage.outputTokens ?? 0,
|
|
656
|
-
reasoningTokens: usage.reasoningTokens ?? 0,
|
|
679
|
+
reasoningTokens: usage.outputTokenDetails?.reasoningTokens ?? 0,
|
|
657
680
|
totalTokens: usage.totalTokens ?? 0,
|
|
658
681
|
},
|
|
659
682
|
userId: auth.user.id,
|
|
@@ -664,7 +687,7 @@ export function createPOST(callbacks: MindRouteCallbacks) {
|
|
|
664
687
|
inputTokens: usage.inputTokens ?? 0,
|
|
665
688
|
modelId: resolvedModelId,
|
|
666
689
|
outputTokens: usage.outputTokens ?? 0,
|
|
667
|
-
reasoningTokens: usage.reasoningTokens ?? 0,
|
|
690
|
+
reasoningTokens: usage.outputTokenDetails?.reasoningTokens ?? 0,
|
|
668
691
|
userId: auth.user.id,
|
|
669
692
|
wsId: billingWsId ?? access.wsId,
|
|
670
693
|
});
|
|
@@ -542,12 +542,9 @@ export function calculateIdealStartTimeForTask(
|
|
|
542
542
|
now: Date
|
|
543
543
|
): Date {
|
|
544
544
|
// Effective slot start (must be >= now, rounded to next 15-min)
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
} else {
|
|
549
|
-
effectiveSlotStart = roundToNext15Minutes(slot.start);
|
|
550
|
-
}
|
|
545
|
+
const effectiveSlotStart = roundToNext15Minutes(
|
|
546
|
+
now > slot.start ? now : slot.start
|
|
547
|
+
);
|
|
551
548
|
|
|
552
549
|
// Latest possible start time to fit the duration
|
|
553
550
|
const latestStart = new Date(slot.end.getTime() - duration * 60000);
|
|
@@ -123,7 +123,7 @@ function extractMeteredUsage(usage: LanguageModelUsage): MeteredUsage {
|
|
|
123
123
|
inputTokens: normalizeTokenCount(usage.inputTokens),
|
|
124
124
|
outputTokens: normalizeTokenCount(usage.outputTokens),
|
|
125
125
|
reasoningTokens: normalizeTokenCount(
|
|
126
|
-
usage.outputTokenDetails.reasoningTokens
|
|
126
|
+
usage.outputTokenDetails.reasoningTokens
|
|
127
127
|
),
|
|
128
128
|
};
|
|
129
129
|
}
|
|
@@ -2,6 +2,7 @@ import { google } from '@ai-sdk/google';
|
|
|
2
2
|
import { generateText, stepCountIs } from 'ai';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { withAiMemory } from '../../memory';
|
|
5
|
+
import { createGoogleSearchToolSet } from '../google-search-tool';
|
|
5
6
|
import type { MiraToolContext } from '../mira-tools';
|
|
6
7
|
|
|
7
8
|
const SEARCH_WRAPPER_MODEL = 'gemini-3.1-flash-lite';
|
|
@@ -85,9 +86,7 @@ async function runGoogleSearchWrapper(
|
|
|
85
86
|
userId: ctx.userId,
|
|
86
87
|
wsId: ctx.workspaceContext?.wsId ?? ctx.wsId,
|
|
87
88
|
}),
|
|
88
|
-
tools:
|
|
89
|
-
google_search: google.tools.googleSearch({}),
|
|
90
|
-
},
|
|
89
|
+
tools: createGoogleSearchToolSet(),
|
|
91
90
|
prompt,
|
|
92
91
|
stopWhen: stepCountIs(4),
|
|
93
92
|
...(forceTool ? { toolChoice: 'required' as const } : {}),
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { google } from '@ai-sdk/google';
|
|
2
|
+
import type { ToolSet } from 'ai';
|
|
3
|
+
|
|
4
|
+
export function createGoogleSearchToolSet(): ToolSet {
|
|
5
|
+
return {
|
|
6
|
+
// Google executes this provider tool itself; AI SDK's public ToolSet type
|
|
7
|
+
// still narrows provider tools through the local-tool generic union.
|
|
8
|
+
google_search: google.tools.googleSearch({}) as unknown as ToolSet[string],
|
|
9
|
+
};
|
|
10
|
+
}
|