ugly-app 0.1.619 → 0.1.621
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/cli/version.d.ts +1 -1
- package/dist/cli/version.js +1 -1
- package/dist/native/contract-meta.d.ts +7 -0
- package/dist/native/contract-meta.d.ts.map +1 -0
- package/dist/native/contract-meta.js +105 -0
- package/dist/native/contract-meta.js.map +1 -0
- package/dist/native/contract.d.ts +254 -24
- package/dist/native/contract.d.ts.map +1 -1
- package/dist/native/contract.js +4 -15
- package/dist/native/contract.js.map +1 -1
- package/dist/native/facades/net.d.ts +26 -0
- package/dist/native/facades/net.d.ts.map +1 -0
- package/dist/native/facades/net.js +22 -0
- package/dist/native/facades/net.js.map +1 -0
- package/dist/native/facades/process.d.ts +20 -0
- package/dist/native/facades/process.d.ts.map +1 -0
- package/dist/native/facades/process.js +15 -0
- package/dist/native/facades/process.js.map +1 -0
- package/dist/native/facades/studio.d.ts +47 -0
- package/dist/native/facades/studio.d.ts.map +1 -0
- package/dist/native/facades/studio.js +38 -0
- package/dist/native/facades/studio.js.map +1 -0
- package/dist/native/index.d.ts +10 -3
- package/dist/native/index.d.ts.map +1 -1
- package/dist/native/index.js +4 -2
- package/dist/native/index.js.map +1 -1
- package/dist/native/install.d.ts.map +1 -1
- package/dist/native/install.js +10 -8
- package/dist/native/install.js.map +1 -1
- package/dist/native/permissions.d.ts +12 -0
- package/dist/native/permissions.d.ts.map +1 -0
- package/dist/native/permissions.js +47 -0
- package/dist/native/permissions.js.map +1 -0
- package/dist/native/protocol.d.ts +5 -1
- package/dist/native/protocol.d.ts.map +1 -1
- package/dist/native/protocol.js +8 -0
- package/dist/native/protocol.js.map +1 -1
- package/dist/native/web-adapter.d.ts +5 -0
- package/dist/native/web-adapter.d.ts.map +1 -0
- package/dist/native/web-adapter.js +111 -0
- package/dist/native/web-adapter.js.map +1 -0
- package/dist/native/wrapper.d.ts +80 -6
- package/dist/native/wrapper.d.ts.map +1 -1
- package/dist/native/wrapper.js +149 -12
- package/dist/native/wrapper.js.map +1 -1
- package/dist/search/server/WebRetriever.d.ts +7 -0
- package/dist/search/server/WebRetriever.d.ts.map +1 -1
- package/dist/search/server/WebRetriever.js +2 -2
- package/dist/search/server/WebRetriever.js.map +1 -1
- package/dist/server/ai/providers/UglyBotWebSearchProvider.d.ts.map +1 -1
- package/dist/server/ai/providers/UglyBotWebSearchProvider.js +11 -8
- package/dist/server/ai/providers/UglyBotWebSearchProvider.js.map +1 -1
- package/dist/server/ai/types.d.ts +7 -0
- package/dist/server/ai/types.d.ts.map +1 -1
- package/dist/server/ai/types.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/version.ts +1 -1
- package/src/native/contract-meta.test.ts +23 -0
- package/src/native/contract-meta.ts +114 -0
- package/src/native/contract.test.ts +13 -7
- package/src/native/contract.ts +130 -32
- package/src/native/facades/facades.test.ts +35 -0
- package/src/native/facades/net.ts +50 -0
- package/src/native/facades/process.ts +37 -0
- package/src/native/facades/studio.ts +73 -0
- package/src/native/index.ts +24 -3
- package/src/native/install.test.ts +8 -0
- package/src/native/install.ts +12 -8
- package/src/native/permissions.test.ts +42 -0
- package/src/native/permissions.ts +53 -0
- package/src/native/protocol.test.ts +15 -0
- package/src/native/protocol.ts +10 -1
- package/src/native/web-adapter.test.ts +27 -0
- package/src/native/web-adapter.ts +118 -0
- package/src/native/wrapper.test.ts +22 -0
- package/src/native/wrapper.ts +163 -13
- package/src/search/server/WebRetriever.ts +9 -4
- package/src/server/ai/providers/UglyBotWebSearchProvider.ts +13 -2
- package/src/server/ai/types.ts +7 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { Channel, NativeEvent, DataOf, SafeAreaInsets } from './contract.js';
|
|
2
|
+
import { fallbackFor } from './contract-meta.js';
|
|
3
|
+
import { NativeUnavailableError } from './protocol.js';
|
|
4
|
+
|
|
5
|
+
type Emit = <E extends NativeEvent>(event: E, data: DataOf<E>) => void;
|
|
6
|
+
|
|
7
|
+
// In-tab alarm timers (web fallback only; cleared on tab close).
|
|
8
|
+
const alarmTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
9
|
+
let gpsWatchId: number | null = null;
|
|
10
|
+
let webAudio: HTMLAudioElement | null = null;
|
|
11
|
+
|
|
12
|
+
function readSafeArea(): SafeAreaInsets {
|
|
13
|
+
const cs = getComputedStyle(document.documentElement);
|
|
14
|
+
const px = (v: string): number => parseInt(v || '0', 10) || 0;
|
|
15
|
+
return {
|
|
16
|
+
top: px(cs.getPropertyValue('--sat')),
|
|
17
|
+
right: 0,
|
|
18
|
+
bottom: 0,
|
|
19
|
+
left: 0,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function webInvoke(channel: Channel, payload: unknown, emit: Emit): Promise<unknown> {
|
|
24
|
+
switch (channel) {
|
|
25
|
+
case 'clipboard.write':
|
|
26
|
+
await navigator.clipboard.writeText((payload as { text: string }).text);
|
|
27
|
+
return undefined;
|
|
28
|
+
case 'clipboard.read':
|
|
29
|
+
return { text: await navigator.clipboard.readText() };
|
|
30
|
+
case 'system.openExternal':
|
|
31
|
+
window.open((payload as { url: string }).url, '_blank', 'noopener');
|
|
32
|
+
return undefined;
|
|
33
|
+
case 'system.share':
|
|
34
|
+
if (navigator.share) await navigator.share(payload as ShareData);
|
|
35
|
+
return undefined;
|
|
36
|
+
case 'badge.set': {
|
|
37
|
+
const n = (payload as { count: number }).count;
|
|
38
|
+
const navAny = navigator as unknown as {
|
|
39
|
+
setAppBadge?: (n?: number) => Promise<void>;
|
|
40
|
+
clearAppBadge?: () => Promise<void>;
|
|
41
|
+
};
|
|
42
|
+
if (n > 0) await navAny.setAppBadge?.(n);
|
|
43
|
+
else await navAny.clearAppBadge?.();
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
case 'ui.getSafeArea':
|
|
47
|
+
return readSafeArea();
|
|
48
|
+
case 'gps.start': {
|
|
49
|
+
if (gpsWatchId != null) navigator.geolocation.clearWatch(gpsWatchId);
|
|
50
|
+
gpsWatchId = navigator.geolocation.watchPosition(
|
|
51
|
+
(pos) =>
|
|
52
|
+
emit('gps.position', {
|
|
53
|
+
lat: pos.coords.latitude,
|
|
54
|
+
lng: pos.coords.longitude,
|
|
55
|
+
accuracy: pos.coords.accuracy,
|
|
56
|
+
timestamp: pos.timestamp,
|
|
57
|
+
altitude: pos.coords.altitude ?? undefined,
|
|
58
|
+
speed: pos.coords.speed ?? undefined,
|
|
59
|
+
course: pos.coords.heading ?? undefined,
|
|
60
|
+
}),
|
|
61
|
+
() => {},
|
|
62
|
+
{ enableHighAccuracy: true },
|
|
63
|
+
);
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
case 'gps.stop':
|
|
67
|
+
if (gpsWatchId != null) {
|
|
68
|
+
navigator.geolocation.clearWatch(gpsWatchId);
|
|
69
|
+
gpsWatchId = null;
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
case 'alarm.schedule': {
|
|
73
|
+
const p = payload as { id: string; delaySeconds: number; title?: string; body?: string };
|
|
74
|
+
const existing = alarmTimers.get(p.id);
|
|
75
|
+
if (existing) clearTimeout(existing);
|
|
76
|
+
alarmTimers.set(
|
|
77
|
+
p.id,
|
|
78
|
+
setTimeout(() => {
|
|
79
|
+
alarmTimers.delete(p.id);
|
|
80
|
+
if ('Notification' in window && Notification.permission === 'granted') {
|
|
81
|
+
new Notification(p.title ?? 'Alarm', { body: p.body });
|
|
82
|
+
}
|
|
83
|
+
emit('alarm.fired', { id: p.id });
|
|
84
|
+
}, p.delaySeconds * 1000),
|
|
85
|
+
);
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
case 'alarm.cancel': {
|
|
89
|
+
const id = (payload as { id?: string }).id;
|
|
90
|
+
if (id) {
|
|
91
|
+
const t = alarmTimers.get(id);
|
|
92
|
+
if (t) clearTimeout(t);
|
|
93
|
+
alarmTimers.delete(id);
|
|
94
|
+
} else {
|
|
95
|
+
for (const t of alarmTimers.values()) clearTimeout(t);
|
|
96
|
+
alarmTimers.clear();
|
|
97
|
+
}
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
case 'alarm.playAudio': {
|
|
101
|
+
const p = payload as { base64: string; mimeType?: string };
|
|
102
|
+
webAudio = new Audio(`data:${p.mimeType ?? 'audio/wav'};base64,${p.base64}`);
|
|
103
|
+
await webAudio.play().catch(() => {});
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
case 'capture.screenshot': {
|
|
107
|
+
// Best-effort capture requires a user-gesture-driven display capture; declared
|
|
108
|
+
// web-capable but gated by a gesture, so treat as unavailable until wired by a caller.
|
|
109
|
+
throw new NativeUnavailableError(channel);
|
|
110
|
+
}
|
|
111
|
+
default: {
|
|
112
|
+
const fb = fallbackFor(channel, 'web');
|
|
113
|
+
if (fb === 'throw') throw new NativeUnavailableError(channel);
|
|
114
|
+
// 'noop' / 'web'-but-unimplemented (liveActivity, tts, push, …) resolve silently.
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -30,4 +30,26 @@ describe('native wrapper', () => {
|
|
|
30
30
|
native.gps.onPosition(cb);
|
|
31
31
|
expect(subscribe).toHaveBeenCalledWith('gps.position', cb);
|
|
32
32
|
});
|
|
33
|
+
|
|
34
|
+
it('capture/clipboard (non-gated) invoke the right channels', async () => {
|
|
35
|
+
const { invoke } = installFakeNative();
|
|
36
|
+
const { native } = await import('./wrapper');
|
|
37
|
+
await native.capture.screenshot();
|
|
38
|
+
await native.clipboard.write({ text: 'a' });
|
|
39
|
+
expect(invoke).toHaveBeenCalledWith('capture.screenshot', undefined);
|
|
40
|
+
expect(invoke).toHaveBeenCalledWith('clipboard.write', { text: 'a' });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('fs.readFile ensures the fs permission then invokes', async () => {
|
|
44
|
+
const granted: Record<string, string> = { fs: 'full' };
|
|
45
|
+
const invoke = vi.fn(async (channel: string) => {
|
|
46
|
+
if (channel === 'permissions.query') return { granted: { ...granted } };
|
|
47
|
+
if (channel === 'fs.readFile') return { content: 'hello' };
|
|
48
|
+
return undefined;
|
|
49
|
+
});
|
|
50
|
+
(globalThis as unknown as { UglyNative: unknown }).UglyNative = { platform: 'desktop', invoke, subscribe: () => () => {} };
|
|
51
|
+
const { native } = await import('./wrapper');
|
|
52
|
+
expect(await native.fs.readFile('/x')).toBe('hello');
|
|
53
|
+
expect(invoke).toHaveBeenCalledWith('fs.readFile', { path: '/x' });
|
|
54
|
+
});
|
|
33
55
|
});
|
package/src/native/wrapper.ts
CHANGED
|
@@ -1,28 +1,178 @@
|
|
|
1
|
-
import type { DataOf, PayloadOf } from './contract.js';
|
|
1
|
+
import type { Channel, DataOf, PayloadOf, ResultOf } from './contract.js';
|
|
2
2
|
import type { NativePlatform } from './protocol.js';
|
|
3
|
+
import { NativeUnavailableError } from './protocol.js';
|
|
3
4
|
import { installUglyNative } from './install.js';
|
|
5
|
+
import { createPermissions } from './permissions.js';
|
|
6
|
+
import { createProcessFacade } from './facades/process.js';
|
|
7
|
+
import { createNetFacade } from './facades/net.js';
|
|
8
|
+
import { createStudioFacade } from './facades/studio.js';
|
|
4
9
|
|
|
5
10
|
const n = () => installUglyNative();
|
|
11
|
+
const inv = <C extends Channel>(c: C, p: PayloadOf<C>): Promise<ResultOf<C>> =>
|
|
12
|
+
n().invoke(c, p) as Promise<ResultOf<C>>;
|
|
13
|
+
|
|
14
|
+
export const permissions = createPermissions((c, p) => n().invoke(c as never, p as never), n().platform);
|
|
15
|
+
|
|
16
|
+
// Desktop daemon bridges (process/net/studio + fs bytes) hang off window.UglyDesktop.
|
|
17
|
+
interface DesktopBridges {
|
|
18
|
+
process?: unknown;
|
|
19
|
+
net?: unknown;
|
|
20
|
+
studio?: unknown;
|
|
21
|
+
fsBytes?: {
|
|
22
|
+
readFileBytes(path: string): Promise<Uint8Array>;
|
|
23
|
+
writeFileBytes(path: string, bytes: Uint8Array): Promise<void>;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const desktopBridge = (): DesktopBridges | undefined =>
|
|
27
|
+
(globalThis as unknown as { UglyDesktop?: DesktopBridges }).UglyDesktop;
|
|
6
28
|
|
|
7
29
|
export const native = {
|
|
30
|
+
permissions,
|
|
31
|
+
capture: {
|
|
32
|
+
screenshot: () => inv('capture.screenshot', undefined),
|
|
33
|
+
},
|
|
8
34
|
gps: {
|
|
9
|
-
start: (p: PayloadOf<'gps.start'>) =>
|
|
10
|
-
stop: () =>
|
|
35
|
+
start: (p: PayloadOf<'gps.start'>) => inv('gps.start', p),
|
|
36
|
+
stop: () => inv('gps.stop', undefined),
|
|
11
37
|
onPosition: (cb: (d: DataOf<'gps.position'>) => void) => n().subscribe('gps.position', cb),
|
|
12
38
|
},
|
|
39
|
+
liveActivity: {
|
|
40
|
+
startWorkout: (p: PayloadOf<'liveActivity.startWorkout'>) => inv('liveActivity.startWorkout', p),
|
|
41
|
+
updateWorkout: (p: PayloadOf<'liveActivity.updateWorkout'>) => inv('liveActivity.updateWorkout', p),
|
|
42
|
+
endWorkout: () => inv('liveActivity.endWorkout', undefined),
|
|
43
|
+
startCardio: (p: PayloadOf<'liveActivity.startCardio'>) => inv('liveActivity.startCardio', p),
|
|
44
|
+
updateCardio: (p: PayloadOf<'liveActivity.updateCardio'>) => inv('liveActivity.updateCardio', p),
|
|
45
|
+
endCardio: () => inv('liveActivity.endCardio', undefined),
|
|
46
|
+
},
|
|
13
47
|
alarm: {
|
|
14
|
-
schedule: (p: PayloadOf<'alarm.schedule'>) =>
|
|
15
|
-
cancel: (p: PayloadOf<'alarm.cancel'>) =>
|
|
16
|
-
playAudio: (p: PayloadOf<'alarm.playAudio'>) =>
|
|
48
|
+
schedule: (p: PayloadOf<'alarm.schedule'>) => inv('alarm.schedule', p),
|
|
49
|
+
cancel: (p: PayloadOf<'alarm.cancel'>) => inv('alarm.cancel', p),
|
|
50
|
+
playAudio: (p: PayloadOf<'alarm.playAudio'>) => inv('alarm.playAudio', p),
|
|
17
51
|
onFired: (cb: (d: DataOf<'alarm.fired'>) => void) => n().subscribe('alarm.fired', cb),
|
|
18
52
|
},
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
53
|
+
tts: {
|
|
54
|
+
start: (p: PayloadOf<'tts.start'>) => inv('tts.start', p),
|
|
55
|
+
pause: () => inv('tts.pause', undefined),
|
|
56
|
+
resume: () => inv('tts.resume', undefined),
|
|
57
|
+
stop: () => inv('tts.stop', undefined),
|
|
58
|
+
onState: (cb: (d: DataOf<'tts.state'>) => void) => n().subscribe('tts.state', cb),
|
|
59
|
+
},
|
|
60
|
+
push: {
|
|
61
|
+
register: () => inv('push.register', undefined),
|
|
62
|
+
onToken: (cb: (d: DataOf<'push.token'>) => void) => n().subscribe('push.token', cb),
|
|
63
|
+
onNotificationClick: (cb: (d: DataOf<'push.notificationClick'>) => void) =>
|
|
64
|
+
n().subscribe('push.notificationClick', cb),
|
|
65
|
+
},
|
|
66
|
+
clipboard: {
|
|
67
|
+
write: (p: PayloadOf<'clipboard.write'>) => inv('clipboard.write', p),
|
|
68
|
+
read: () => inv('clipboard.read', undefined),
|
|
69
|
+
},
|
|
70
|
+
system: {
|
|
71
|
+
openExternal: (p: PayloadOf<'system.openExternal'>) => inv('system.openExternal', p),
|
|
72
|
+
share: (p: PayloadOf<'system.share'>) => inv('system.share', p),
|
|
73
|
+
},
|
|
74
|
+
badge: {
|
|
75
|
+
set: (p: PayloadOf<'badge.set'>) => inv('badge.set', p),
|
|
76
|
+
},
|
|
77
|
+
ui: {
|
|
78
|
+
getSafeArea: () => inv('ui.getSafeArea', undefined),
|
|
79
|
+
onKeyboardHeight: (cb: (d: DataOf<'keyboard.height'>) => void) => n().subscribe('keyboard.height', cb),
|
|
80
|
+
onSafeAreaInsets: (cb: (d: DataOf<'safeArea.insets'>) => void) => n().subscribe('safeArea.insets', cb),
|
|
81
|
+
},
|
|
82
|
+
fs: {
|
|
83
|
+
readFile: async (path: string) => {
|
|
84
|
+
await permissions.ensure('fs');
|
|
85
|
+
return (await inv('fs.readFile', { path })).content;
|
|
86
|
+
},
|
|
87
|
+
writeFile: async (path: string, content: string) => {
|
|
88
|
+
await permissions.ensure('fs');
|
|
89
|
+
return inv('fs.writeFile', { path, content });
|
|
90
|
+
},
|
|
91
|
+
mkdir: async (path: string, recursive = false) => {
|
|
92
|
+
await permissions.ensure('fs');
|
|
93
|
+
return inv('fs.mkdir', { path, recursive });
|
|
94
|
+
},
|
|
95
|
+
rm: async (path: string, opts: { recursive?: boolean; force?: boolean } = {}) => {
|
|
96
|
+
await permissions.ensure('fs');
|
|
97
|
+
return inv('fs.rm', { path, ...opts });
|
|
98
|
+
},
|
|
99
|
+
rename: async (from: string, to: string) => {
|
|
100
|
+
await permissions.ensure('fs');
|
|
101
|
+
return inv('fs.rename', { from, to });
|
|
102
|
+
},
|
|
103
|
+
readdir: async (path: string) => {
|
|
104
|
+
await permissions.ensure('fs');
|
|
105
|
+
return (await inv('fs.readdir', { path })).entries;
|
|
106
|
+
},
|
|
107
|
+
stat: async (path: string) => {
|
|
108
|
+
await permissions.ensure('fs');
|
|
109
|
+
return inv('fs.stat', { path });
|
|
110
|
+
},
|
|
111
|
+
exists: async (path: string) => {
|
|
112
|
+
await permissions.ensure('fs');
|
|
113
|
+
return (await inv('fs.exists', { path })).exists;
|
|
114
|
+
},
|
|
115
|
+
realpath: async (path: string) => {
|
|
116
|
+
await permissions.ensure('fs');
|
|
117
|
+
return (await inv('fs.realpath', { path })).path;
|
|
118
|
+
},
|
|
119
|
+
readFileBytes: async (path: string): Promise<Uint8Array> => {
|
|
120
|
+
await permissions.ensure('fs');
|
|
121
|
+
const fb = desktopBridge()?.fsBytes;
|
|
122
|
+
if (!fb) throw new NativeUnavailableError('fs.readFileBytes');
|
|
123
|
+
return fb.readFileBytes(path);
|
|
124
|
+
},
|
|
125
|
+
writeFileBytes: async (path: string, bytes: Uint8Array): Promise<void> => {
|
|
126
|
+
await permissions.ensure('fs');
|
|
127
|
+
const fb = desktopBridge()?.fsBytes;
|
|
128
|
+
if (!fb) throw new NativeUnavailableError('fs.writeFileBytes');
|
|
129
|
+
return fb.writeFileBytes(path, bytes);
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
process: createProcessFacade(() => desktopBridge()?.process as never, n().platform, () => permissions.ensure('process')),
|
|
133
|
+
net: createNetFacade(() => desktopBridge()?.net as never, n().platform, () => permissions.ensure('net')),
|
|
134
|
+
studio: createStudioFacade(() => desktopBridge()?.studio as never, n().platform),
|
|
135
|
+
secrets: {
|
|
136
|
+
get: async (key: string) => {
|
|
137
|
+
await permissions.ensure('secrets');
|
|
138
|
+
return (await inv('secrets.get', { key })).value;
|
|
139
|
+
},
|
|
140
|
+
set: async (key: string, value: string) => {
|
|
141
|
+
await permissions.ensure('secrets');
|
|
142
|
+
return inv('secrets.set', { key, value });
|
|
143
|
+
},
|
|
144
|
+
delete: async (key: string) => {
|
|
145
|
+
await permissions.ensure('secrets');
|
|
146
|
+
return inv('secrets.delete', { key });
|
|
147
|
+
},
|
|
148
|
+
list: async () => {
|
|
149
|
+
await permissions.ensure('secrets');
|
|
150
|
+
return (await inv('secrets.list', undefined)).keys;
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
serve: {
|
|
154
|
+
start: async (dir?: string) => {
|
|
155
|
+
await permissions.ensure('serve');
|
|
156
|
+
return inv('serve.start', { dir });
|
|
157
|
+
},
|
|
158
|
+
stop: async (port: number) => {
|
|
159
|
+
await permissions.ensure('serve');
|
|
160
|
+
return inv('serve.stop', { port });
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
workers: {
|
|
164
|
+
register: async (path: string, boot = false) => {
|
|
165
|
+
await permissions.ensure('workers');
|
|
166
|
+
return (await inv('workers.register', { path, boot })).id;
|
|
167
|
+
},
|
|
168
|
+
unregister: async (id: string) => {
|
|
169
|
+
await permissions.ensure('workers');
|
|
170
|
+
return inv('workers.unregister', { id });
|
|
171
|
+
},
|
|
172
|
+
list: async () => {
|
|
173
|
+
await permissions.ensure('workers');
|
|
174
|
+
return (await inv('workers.list', undefined)).ids;
|
|
175
|
+
},
|
|
26
176
|
},
|
|
27
177
|
};
|
|
28
178
|
|
|
@@ -11,17 +11,22 @@ import type { Retriever, RetrievalResult, RetrieveOptions } from '../shared/Retr
|
|
|
11
11
|
export interface WebSearchLike {
|
|
12
12
|
search(
|
|
13
13
|
params: { query: string; limit?: number },
|
|
14
|
-
options?: { userId?: string },
|
|
14
|
+
options?: { userId?: string; token?: string },
|
|
15
15
|
): Promise<{ items: { url: string; title: string; snippet: string }[] }>;
|
|
16
16
|
summarize?(
|
|
17
17
|
params: { url?: string; text?: string },
|
|
18
|
-
options?: { userId?: string },
|
|
18
|
+
options?: { userId?: string; token?: string },
|
|
19
19
|
): Promise<{ output: string }>;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export interface WebRetrieverOptions {
|
|
23
23
|
/** Bill the search proxy to this user (charge context). */
|
|
24
24
|
userId?: string;
|
|
25
|
+
/**
|
|
26
|
+
* The end user's ugly.bot session JWT. When set, the search proxy bills the
|
|
27
|
+
* USER (not the app owner). Omit for the default owner-billed behavior.
|
|
28
|
+
*/
|
|
29
|
+
userToken?: string;
|
|
25
30
|
/** Default result count. */
|
|
26
31
|
limit?: number;
|
|
27
32
|
}
|
|
@@ -46,7 +51,7 @@ export class WebRetriever implements Retriever {
|
|
|
46
51
|
const limit = o?.limit ?? this.opts.limit ?? 8;
|
|
47
52
|
const { items } = await this.provider.search(
|
|
48
53
|
{ query, limit },
|
|
49
|
-
{ userId: this.opts.userId },
|
|
54
|
+
{ userId: this.opts.userId, token: this.opts.userToken },
|
|
50
55
|
);
|
|
51
56
|
const n = Math.max(items.length, 1);
|
|
52
57
|
return items.map((it, i) => ({
|
|
@@ -69,7 +74,7 @@ export class WebRetriever implements Retriever {
|
|
|
69
74
|
try {
|
|
70
75
|
const { output } = await this.provider.summarize(
|
|
71
76
|
{ url: result.source.url },
|
|
72
|
-
{ userId: this.opts.userId },
|
|
77
|
+
{ userId: this.opts.userId, token: this.opts.userToken },
|
|
73
78
|
);
|
|
74
79
|
if (output) return output;
|
|
75
80
|
} catch {
|
|
@@ -23,9 +23,15 @@ import type { WebSearchOptions, WebSearchProvider } from '../types.js';
|
|
|
23
23
|
|
|
24
24
|
const DEFAULT_SEARCH_PROXY_URL = 'https://api.ugly.bot/v1/search';
|
|
25
25
|
|
|
26
|
-
function resolveSearchEndpoint(
|
|
26
|
+
function resolveSearchEndpoint(
|
|
27
|
+
op: string,
|
|
28
|
+
userToken?: string,
|
|
29
|
+
): { url: string; token: string } {
|
|
27
30
|
const base = process.env['SEARCH_PROXY_URL'] ?? DEFAULT_SEARCH_PROXY_URL;
|
|
31
|
+
// A user session JWT (when supplied) bills the user via resolveChargePayer;
|
|
32
|
+
// otherwise fall back to the app's owner-billed proxy token.
|
|
28
33
|
const token =
|
|
34
|
+
userToken ??
|
|
29
35
|
process.env['SEARCH_PROXY_TOKEN'] ??
|
|
30
36
|
process.env['UGLY_BOT_TOKEN'] ??
|
|
31
37
|
'';
|
|
@@ -42,8 +48,9 @@ function resolveSearchEndpoint(op: string): { url: string; token: string } {
|
|
|
42
48
|
async function searchProxyRequest<T>(
|
|
43
49
|
op: string,
|
|
44
50
|
body: Record<string, unknown>,
|
|
51
|
+
userToken?: string,
|
|
45
52
|
): Promise<T> {
|
|
46
|
-
const { url, token } = resolveSearchEndpoint(op);
|
|
53
|
+
const { url, token } = resolveSearchEndpoint(op, userToken);
|
|
47
54
|
const controller = new AbortController();
|
|
48
55
|
const timer = setTimeout(() => controller.abort(), 30_000);
|
|
49
56
|
try {
|
|
@@ -90,6 +97,7 @@ export const uglyBotWebSearchProvider: WebSearchProvider = {
|
|
|
90
97
|
{ query: params.query, limit: params.limit },
|
|
91
98
|
options?.userId,
|
|
92
99
|
),
|
|
100
|
+
options?.token,
|
|
93
101
|
);
|
|
94
102
|
},
|
|
95
103
|
|
|
@@ -103,6 +111,7 @@ export const uglyBotWebSearchProvider: WebSearchProvider = {
|
|
|
103
111
|
{ url: params.url, text: params.text },
|
|
104
112
|
options?.userId,
|
|
105
113
|
),
|
|
114
|
+
options?.token,
|
|
106
115
|
);
|
|
107
116
|
return {
|
|
108
117
|
output: result.summary,
|
|
@@ -117,6 +126,7 @@ export const uglyBotWebSearchProvider: WebSearchProvider = {
|
|
|
117
126
|
return searchProxyRequest<WebSearchOutput>(
|
|
118
127
|
'enrich-web',
|
|
119
128
|
withChargeContext({ query: params.query }, options?.userId),
|
|
129
|
+
options?.token,
|
|
120
130
|
);
|
|
121
131
|
},
|
|
122
132
|
|
|
@@ -127,6 +137,7 @@ export const uglyBotWebSearchProvider: WebSearchProvider = {
|
|
|
127
137
|
return searchProxyRequest<WebSearchOutput>(
|
|
128
138
|
'enrich-news',
|
|
129
139
|
withChargeContext({ query: params.query }, options?.userId),
|
|
140
|
+
options?.token,
|
|
130
141
|
);
|
|
131
142
|
},
|
|
132
143
|
};
|
package/src/server/ai/types.ts
CHANGED
|
@@ -210,6 +210,13 @@ export interface ImageGenProvider {
|
|
|
210
210
|
|
|
211
211
|
export interface WebSearchOptions {
|
|
212
212
|
userId?: string;
|
|
213
|
+
/**
|
|
214
|
+
* The end user's ugly.bot session JWT. When set, the search proxy is called
|
|
215
|
+
* with THIS token instead of the app's `SEARCH_PROXY_TOKEN`, so ugly.bot bills
|
|
216
|
+
* the user (via `resolveChargePayer`) instead of the app owner. Omit to keep
|
|
217
|
+
* the default owner-billed behavior.
|
|
218
|
+
*/
|
|
219
|
+
token?: string;
|
|
213
220
|
}
|
|
214
221
|
|
|
215
222
|
export interface WebSearchProvider {
|