@agent-native/core 0.84.56 → 0.84.58
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +14 -0
- package/corpus/core/docs/content/locales/ar-SA/template-analytics.mdx +2 -0
- package/corpus/core/docs/content/locales/ar-SA/template-clips.mdx +2 -0
- package/corpus/core/docs/content/locales/de-DE/template-analytics.mdx +2 -0
- package/corpus/core/docs/content/locales/de-DE/template-clips.mdx +2 -0
- package/corpus/core/docs/content/locales/es-ES/template-analytics.mdx +2 -0
- package/corpus/core/docs/content/locales/es-ES/template-clips.mdx +2 -0
- package/corpus/core/docs/content/locales/fr-FR/template-analytics.mdx +2 -0
- package/corpus/core/docs/content/locales/fr-FR/template-clips.mdx +2 -0
- package/corpus/core/docs/content/locales/hi-IN/template-analytics.mdx +2 -0
- package/corpus/core/docs/content/locales/hi-IN/template-clips.mdx +2 -0
- package/corpus/core/docs/content/locales/ja-JP/template-analytics.mdx +2 -0
- package/corpus/core/docs/content/locales/ja-JP/template-clips.mdx +2 -0
- package/corpus/core/docs/content/locales/ko-KR/template-analytics.mdx +2 -0
- package/corpus/core/docs/content/locales/ko-KR/template-clips.mdx +2 -0
- package/corpus/core/docs/content/locales/pt-BR/template-analytics.mdx +2 -0
- package/corpus/core/docs/content/locales/pt-BR/template-clips.mdx +2 -0
- package/corpus/core/docs/content/locales/zh-CN/template-analytics.mdx +2 -0
- package/corpus/core/docs/content/locales/zh-CN/template-clips.mdx +2 -0
- package/corpus/core/docs/content/locales/zh-TW/template-analytics.mdx +2 -0
- package/corpus/core/docs/content/locales/zh-TW/template-clips.mdx +2 -0
- package/corpus/core/docs/content/template-analytics.mdx +2 -0
- package/corpus/core/docs/content/template-clips.mdx +22 -10
- package/corpus/core/docs/content/tracking.mdx +2 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/action.ts +11 -0
- package/corpus/core/src/agent/production-agent.ts +24 -46
- package/corpus/core/src/client/AssistantChat.tsx +69 -12
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +1 -1
- package/corpus/core/src/client/conversation/use-near-bottom-autoscroll.ts +45 -2
- package/corpus/core/src/coding-tools/run-code.ts +1 -0
- package/corpus/core/src/integrations/webhook-handler.ts +10 -9
- package/corpus/core/src/server/action-discovery.ts +3 -0
- package/corpus/core/src/server/credential-provider.ts +11 -6
- package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +8 -0
- package/corpus/templates/analytics/AGENTS.md +5 -0
- package/corpus/templates/analytics/actions/create-session-replay-agent-link.ts +31 -0
- package/corpus/templates/analytics/app/i18n/zh-TW.ts +2 -0
- package/corpus/templates/analytics/app/i18n-data.ts +20 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +65 -3
- package/corpus/templates/analytics/changelog/2026-07-02-session-replays-can-be-copied-as-temporary-private-links-for-agents.md +6 -0
- package/corpus/templates/analytics/server/handlers/session-replay.ts +78 -0
- package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +29 -0
- package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +313 -0
- package/corpus/templates/analytics/server/lib/session-replay.ts +198 -3
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +9 -1
- package/corpus/templates/analytics/server/routes/[...page].get.ts +104 -1
- package/corpus/templates/analytics/server/routes/api/session-replay/agent-context.json.get.ts +48 -0
- package/corpus/templates/analytics/server/routes/api/session-replay/agent-events.json.get.ts +63 -0
- package/corpus/templates/analytics/shared/session-replay-agent-access.ts +10 -0
- package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +14 -8
- package/corpus/templates/clips/AGENTS.md +3 -2
- package/corpus/templates/clips/actions/create-recording-agent-link.ts +90 -0
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +43 -46
- package/corpus/templates/clips/app/components/sharing/share-ui.tsx +5 -3
- package/corpus/templates/clips/app/i18n/ar-SA.ts +1 -1
- package/corpus/templates/clips/app/i18n/de-DE.ts +1 -1
- package/corpus/templates/clips/app/i18n/en-US.ts +1 -1
- package/corpus/templates/clips/app/i18n/es-ES.ts +1 -1
- package/corpus/templates/clips/app/i18n/fr-FR.ts +1 -1
- package/corpus/templates/clips/app/i18n/hi-IN.ts +1 -1
- package/corpus/templates/clips/app/i18n/ja-JP.ts +1 -1
- package/corpus/templates/clips/app/i18n/ko-KR.ts +1 -1
- package/corpus/templates/clips/app/i18n/pt-BR.ts +1 -1
- package/corpus/templates/clips/app/i18n/zh-CN.ts +1 -1
- package/corpus/templates/clips/app/i18n/zh-TW.ts +1 -1
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +100 -46
- package/corpus/templates/clips/changelog/2026-07-02-private-recordings-can-be-shared-with-agents-through-temporary-links-without-making-them-public.md +6 -0
- package/corpus/templates/clips/server/lib/public-agent-context.ts +26 -13
- package/corpus/templates/clips/server/routes/api/agent-context.json.get.ts +2 -1
- package/corpus/templates/clips/server/routes/api/agent-frame.jpg.get.ts +2 -1
- package/corpus/templates/clips/server/routes/api/agent-transcript.json.get.ts +2 -1
- package/corpus/templates/clips/server/routes/api/public-recording.get.ts +7 -2
- package/corpus/templates/clips/shared/agent-context.ts +5 -0
- package/corpus/templates/design/actions/list-design-extensions.ts +2 -2
- package/corpus/templates/design/app/components/design/CodeWorkbenchHost.tsx +11 -89
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +314 -181
- package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +92 -134
- package/corpus/templates/design/app/components/design/EditPanel.tsx +93 -19
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +18 -26
- package/corpus/templates/design/app/components/design/TokensPanel.tsx +220 -192
- package/corpus/templates/design/app/components/design/code-workbench-theme.ts +150 -0
- package/corpus/templates/design/app/i18n/zh-TW.ts +5 -7
- package/corpus/templates/design/app/i18n-data.ts +61 -77
- package/corpus/templates/design/app/lib/design-import.ts +4 -1
- package/corpus/templates/design/app/pages/DesignEditor.tsx +97 -48
- package/corpus/templates/design/changelog/2026-07-02-design-inspector-and-canvas-controls-stay-consistent-when-se.md +6 -0
- package/corpus/templates/design/changelog/2026-07-02-fixed-a-crash-when-opening-the-code-editor.md +6 -0
- package/corpus/templates/design/changelog/2026-07-02-fixed-motion-drawer-open-transition.md +6 -0
- package/corpus/templates/design/changelog/2026-07-02-improved-design-assets-panel.md +6 -0
- package/dist/action.d.ts +8 -0
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +3 -0
- package/dist/action.js.map +1 -1
- package/dist/agent/production-agent.d.ts +10 -11
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +21 -44
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +1 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +61 -11
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +1 -1
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/conversation/use-near-bottom-autoscroll.d.ts.map +1 -1
- package/dist/client/conversation/use-near-bottom-autoscroll.js +43 -2
- package/dist/client/conversation/use-near-bottom-autoscroll.js.map +1 -1
- package/dist/coding-tools/run-code.d.ts.map +1 -1
- package/dist/coding-tools/run-code.js +1 -0
- package/dist/coding-tools/run-code.js.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/integrations/webhook-handler.d.ts +1 -0
- package/dist/integrations/webhook-handler.d.ts.map +1 -1
- package/dist/integrations/webhook-handler.js +9 -9
- package/dist/integrations/webhook-handler.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/action-discovery.d.ts.map +1 -1
- package/dist/server/action-discovery.js +3 -0
- package/dist/server/action-discovery.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/credential-provider.d.ts +4 -0
- package/dist/server/credential-provider.d.ts.map +1 -1
- package/dist/server/credential-provider.js +13 -7
- package/dist/server/credential-provider.js.map +1 -1
- package/docs/content/locales/ar-SA/template-analytics.mdx +2 -0
- package/docs/content/locales/ar-SA/template-clips.mdx +2 -0
- package/docs/content/locales/de-DE/template-analytics.mdx +2 -0
- package/docs/content/locales/de-DE/template-clips.mdx +2 -0
- package/docs/content/locales/es-ES/template-analytics.mdx +2 -0
- package/docs/content/locales/es-ES/template-clips.mdx +2 -0
- package/docs/content/locales/fr-FR/template-analytics.mdx +2 -0
- package/docs/content/locales/fr-FR/template-clips.mdx +2 -0
- package/docs/content/locales/hi-IN/template-analytics.mdx +2 -0
- package/docs/content/locales/hi-IN/template-clips.mdx +2 -0
- package/docs/content/locales/ja-JP/template-analytics.mdx +2 -0
- package/docs/content/locales/ja-JP/template-clips.mdx +2 -0
- package/docs/content/locales/ko-KR/template-analytics.mdx +2 -0
- package/docs/content/locales/ko-KR/template-clips.mdx +2 -0
- package/docs/content/locales/pt-BR/template-analytics.mdx +2 -0
- package/docs/content/locales/pt-BR/template-clips.mdx +2 -0
- package/docs/content/locales/zh-CN/template-analytics.mdx +2 -0
- package/docs/content/locales/zh-CN/template-clips.mdx +2 -0
- package/docs/content/locales/zh-TW/template-analytics.mdx +2 -0
- package/docs/content/locales/zh-TW/template-clips.mdx +2 -0
- package/docs/content/template-analytics.mdx +2 -0
- package/docs/content/template-clips.mdx +22 -10
- package/docs/content/tracking.mdx +2 -0
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "@agent-native/core/server";
|
|
8
8
|
|
|
9
9
|
import actionsRegistry from "../../.generated/actions-registry.js";
|
|
10
|
+
import { applyAnalyticsPlanModePolicy } from "../lib/agent-chat-plan-mode";
|
|
10
11
|
import { renderDataDictionary } from "../lib/data-dictionary-context";
|
|
11
12
|
import {
|
|
12
13
|
failedDataQueryAttemptMessage,
|
|
@@ -65,6 +66,11 @@ const INITIAL_TOOL_NAMES = [
|
|
|
65
66
|
"navigate",
|
|
66
67
|
];
|
|
67
68
|
|
|
69
|
+
export {
|
|
70
|
+
applyAnalyticsPlanModePolicy,
|
|
71
|
+
PLAN_MODE_ACT_ONLY_TOOLS,
|
|
72
|
+
} from "../lib/agent-chat-plan-mode";
|
|
73
|
+
|
|
68
74
|
function latestUserText(
|
|
69
75
|
messages: AgentLoopFinalResponseGuardContext["messages"],
|
|
70
76
|
): string {
|
|
@@ -160,7 +166,9 @@ function realDataFinalGuard(context: AgentLoopFinalResponseGuardContext) {
|
|
|
160
166
|
|
|
161
167
|
export default createAgentChatPlugin({
|
|
162
168
|
appId: "analytics",
|
|
163
|
-
actions:
|
|
169
|
+
actions: applyAnalyticsPlanModePolicy(
|
|
170
|
+
loadActionsFromStaticRegistry(actionsRegistry),
|
|
171
|
+
),
|
|
164
172
|
initialToolNames: INITIAL_TOOL_NAMES,
|
|
165
173
|
finalResponseGuard: realDataFinalGuard,
|
|
166
174
|
// Enable sandboxed JavaScript execution for analytics data processing.
|
|
@@ -1,5 +1,108 @@
|
|
|
1
1
|
import { createH3SSRHandler } from "@agent-native/core/server/ssr-handler";
|
|
2
|
+
import {
|
|
3
|
+
defineEventHandler,
|
|
4
|
+
getQuery,
|
|
5
|
+
getRequestURL,
|
|
6
|
+
setResponseHeader,
|
|
7
|
+
} from "h3";
|
|
2
8
|
|
|
3
|
-
|
|
9
|
+
import {
|
|
10
|
+
buildSessionReplayAgentContext,
|
|
11
|
+
safeJsonForHtml,
|
|
12
|
+
SESSION_REPLAY_AGENT_ACCESS_PARAM,
|
|
13
|
+
} from "../lib/session-replay-agent-context.js";
|
|
14
|
+
|
|
15
|
+
const ssrHandler = createH3SSRHandler(
|
|
4
16
|
() => import("virtual:react-router/server-build"),
|
|
5
17
|
);
|
|
18
|
+
|
|
19
|
+
function configuredAppBasePath(): string {
|
|
20
|
+
const raw = process.env.VITE_APP_BASE_PATH || process.env.APP_BASE_PATH || "";
|
|
21
|
+
if (!raw || raw === "/") return "";
|
|
22
|
+
const normalized = raw.startsWith("/") ? raw : `/${raw}`;
|
|
23
|
+
return normalized.replace(/\/+$/, "");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function stripAppBasePath(pathname: string): string {
|
|
27
|
+
const basePath = configuredAppBasePath();
|
|
28
|
+
if (!basePath) return pathname;
|
|
29
|
+
if (pathname === basePath) return "/";
|
|
30
|
+
if (pathname.startsWith(`${basePath}/`)) {
|
|
31
|
+
return pathname.slice(basePath.length) || "/";
|
|
32
|
+
}
|
|
33
|
+
return pathname;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sessionRecordingIdFromPath(pathname: string): string | null {
|
|
37
|
+
const match = stripAppBasePath(pathname).match(/^\/sessions\/([^/]+)\/?$/);
|
|
38
|
+
if (!match?.[1]) return null;
|
|
39
|
+
try {
|
|
40
|
+
return decodeURIComponent(match[1]);
|
|
41
|
+
} catch {
|
|
42
|
+
return match[1];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function queryString(value: unknown): string {
|
|
47
|
+
if (typeof value === "string") return value;
|
|
48
|
+
if (Array.isArray(value) && typeof value[0] === "string") return value[0];
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function buildAgentDiscoveryScript(event: any): Promise<string | null> {
|
|
53
|
+
const requestUrl = getRequestURL(event);
|
|
54
|
+
const recordingId = sessionRecordingIdFromPath(requestUrl.pathname);
|
|
55
|
+
if (!recordingId) return null;
|
|
56
|
+
|
|
57
|
+
const query = getQuery(event);
|
|
58
|
+
const token = queryString(query[SESSION_REPLAY_AGENT_ACCESS_PARAM]);
|
|
59
|
+
if (!token) return null;
|
|
60
|
+
|
|
61
|
+
const context = await buildSessionReplayAgentContext({
|
|
62
|
+
recordingId,
|
|
63
|
+
token,
|
|
64
|
+
origin: requestUrl.origin,
|
|
65
|
+
includeTimeline: false,
|
|
66
|
+
}).catch(() => null);
|
|
67
|
+
if (!context) return null;
|
|
68
|
+
|
|
69
|
+
return `<script type="application/agent-native+json" id="analytics-session-replay-agent-context">${safeJsonForHtml(
|
|
70
|
+
context,
|
|
71
|
+
)}</script>`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function injectAgentDiscovery(html: string, script: string): string {
|
|
75
|
+
if (html.includes('id="analytics-session-replay-agent-context"')) {
|
|
76
|
+
return html;
|
|
77
|
+
}
|
|
78
|
+
if (html.includes("</head>")) {
|
|
79
|
+
return html.replace("</head>", `${script}</head>`);
|
|
80
|
+
}
|
|
81
|
+
if (html.includes("</body>")) {
|
|
82
|
+
return html.replace("</body>", `${script}</body>`);
|
|
83
|
+
}
|
|
84
|
+
return `${html}${script}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default defineEventHandler(async (event) => {
|
|
88
|
+
const response = (await ssrHandler(event)) as Response;
|
|
89
|
+
const script = await buildAgentDiscoveryScript(event);
|
|
90
|
+
if (!script) return response;
|
|
91
|
+
|
|
92
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
93
|
+
if (!contentType.includes("text/html")) return response;
|
|
94
|
+
|
|
95
|
+
const html = await response.text();
|
|
96
|
+
const headers = new Headers(response.headers);
|
|
97
|
+
headers.delete("content-length");
|
|
98
|
+
headers.set("Cache-Control", "private, max-age=0, no-store");
|
|
99
|
+
headers.set("Referrer-Policy", "no-referrer");
|
|
100
|
+
setResponseHeader(event, "Cache-Control", "private, max-age=0, no-store");
|
|
101
|
+
setResponseHeader(event, "Referrer-Policy", "no-referrer");
|
|
102
|
+
|
|
103
|
+
return new Response(injectAgentDiscovery(html, script), {
|
|
104
|
+
status: response.status,
|
|
105
|
+
statusText: response.statusText,
|
|
106
|
+
headers,
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineEventHandler,
|
|
3
|
+
getQuery,
|
|
4
|
+
getRequestURL,
|
|
5
|
+
setResponseHeader,
|
|
6
|
+
setResponseStatus,
|
|
7
|
+
} from "h3";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
buildSessionReplayAgentContext,
|
|
11
|
+
SESSION_REPLAY_AGENT_ACCESS_PARAM,
|
|
12
|
+
} from "../../../lib/session-replay-agent-context.js";
|
|
13
|
+
|
|
14
|
+
function queryString(value: unknown): string {
|
|
15
|
+
if (typeof value === "string") return value;
|
|
16
|
+
if (Array.isArray(value) && typeof value[0] === "string") return value[0];
|
|
17
|
+
return "";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function applyAgentJsonHeaders(event: any) {
|
|
21
|
+
setResponseHeader(event, "Content-Type", "application/json; charset=utf-8");
|
|
22
|
+
setResponseHeader(event, "X-Content-Type-Options", "nosniff");
|
|
23
|
+
setResponseHeader(event, "Referrer-Policy", "no-referrer");
|
|
24
|
+
setResponseHeader(event, "Cache-Control", "private, max-age=0, no-store");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default defineEventHandler(async (event) => {
|
|
28
|
+
applyAgentJsonHeaders(event);
|
|
29
|
+
const query = getQuery(event);
|
|
30
|
+
const id = queryString(query.id);
|
|
31
|
+
const token = queryString(query[SESSION_REPLAY_AGENT_ACCESS_PARAM]);
|
|
32
|
+
|
|
33
|
+
if (!id || !token) {
|
|
34
|
+
setResponseStatus(event, 400);
|
|
35
|
+
return { error: "id and agent access token are required" };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
return await buildSessionReplayAgentContext({
|
|
40
|
+
recordingId: id,
|
|
41
|
+
token,
|
|
42
|
+
origin: getRequestURL(event).origin,
|
|
43
|
+
});
|
|
44
|
+
} catch (error: any) {
|
|
45
|
+
setResponseStatus(event, error?.statusCode ?? 400);
|
|
46
|
+
return { error: error?.message || String(error) };
|
|
47
|
+
}
|
|
48
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineEventHandler,
|
|
3
|
+
getQuery,
|
|
4
|
+
setResponseHeader,
|
|
5
|
+
setResponseStatus,
|
|
6
|
+
} from "h3";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
SESSION_REPLAY_AGENT_ACCESS_PARAM,
|
|
10
|
+
verifySessionReplayAgentAccess,
|
|
11
|
+
} from "../../../lib/session-replay-agent-context.js";
|
|
12
|
+
import {
|
|
13
|
+
getSessionReplayTokenizedEvents,
|
|
14
|
+
type SessionReplayEventReadOptions,
|
|
15
|
+
} from "../../../lib/session-replay.js";
|
|
16
|
+
|
|
17
|
+
function queryString(value: unknown): string {
|
|
18
|
+
if (typeof value === "string") return value;
|
|
19
|
+
if (Array.isArray(value) && typeof value[0] === "string") return value[0];
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function queryInt(value: unknown): number | undefined {
|
|
24
|
+
const raw = queryString(value);
|
|
25
|
+
if (!raw || !/^\d+$/.test(raw)) return undefined;
|
|
26
|
+
return Number(raw);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function applyAgentJsonHeaders(event: any) {
|
|
30
|
+
setResponseHeader(event, "Content-Type", "application/json; charset=utf-8");
|
|
31
|
+
setResponseHeader(event, "X-Content-Type-Options", "nosniff");
|
|
32
|
+
setResponseHeader(event, "Referrer-Policy", "no-referrer");
|
|
33
|
+
setResponseHeader(event, "Cache-Control", "private, max-age=0, no-store");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default defineEventHandler(async (event) => {
|
|
37
|
+
applyAgentJsonHeaders(event);
|
|
38
|
+
const query = getQuery(event);
|
|
39
|
+
const id = queryString(query.id);
|
|
40
|
+
const token = queryString(query[SESSION_REPLAY_AGENT_ACCESS_PARAM]);
|
|
41
|
+
|
|
42
|
+
if (!id || !token) {
|
|
43
|
+
setResponseStatus(event, 400);
|
|
44
|
+
return { error: "id and agent access token are required" };
|
|
45
|
+
}
|
|
46
|
+
if (!verifySessionReplayAgentAccess(id, token)) {
|
|
47
|
+
setResponseStatus(event, 401);
|
|
48
|
+
return { error: "Invalid or expired agent access" };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const options: SessionReplayEventReadOptions = {
|
|
52
|
+
startSeq: queryInt(query.startSeq),
|
|
53
|
+
endSeq: queryInt(query.endSeq),
|
|
54
|
+
limit: queryInt(query.limit),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
return await getSessionReplayTokenizedEvents(id, options);
|
|
59
|
+
} catch (error: any) {
|
|
60
|
+
setResponseStatus(event, error?.statusCode ?? 400);
|
|
61
|
+
return { error: error?.message || String(error) };
|
|
62
|
+
}
|
|
63
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const SESSION_REPLAY_AGENT_ACCESS_PARAM = "agent_access";
|
|
2
|
+
|
|
3
|
+
const SESSION_REPLAY_AGENT_ACCESS_TOKEN_PREFIX =
|
|
4
|
+
"analytics-session-replay-agent-context";
|
|
5
|
+
|
|
6
|
+
export function sessionReplayAgentAccessTokenResourceId(
|
|
7
|
+
recordingId: string,
|
|
8
|
+
): string {
|
|
9
|
+
return `${SESSION_REPLAY_AGENT_ACCESS_TOKEN_PREFIX}:${recordingId}`;
|
|
10
|
+
}
|
|
@@ -156,9 +156,10 @@ Required Slack app setup:
|
|
|
156
156
|
- Deploy secrets: `SLACK_CLIENT_ID`, `SLACK_CLIENT_SECRET`, and
|
|
157
157
|
`SLACK_SIGNING_SECRET`
|
|
158
158
|
|
|
159
|
-
## Agent-readable
|
|
159
|
+
## Agent-readable clips
|
|
160
160
|
|
|
161
|
-
|
|
161
|
+
Recordings can expose URLs meant for external agents without handing over raw
|
|
162
|
+
video bytes:
|
|
162
163
|
|
|
163
164
|
| Endpoint | Meaning |
|
|
164
165
|
| -------- | ------- |
|
|
@@ -166,9 +167,14 @@ Public recordings also expose URLs meant for external agents:
|
|
|
166
167
|
| `/api/agent-transcript.json?id=<recordingId>` | Timestamped transcript segments with `startMs`, `endMs`, `timestamp`, `range`, `text`, and optional `source` |
|
|
167
168
|
| `/api/agent-frame.jpg?id=<recordingId>&atMs=<ms>` | JPEG frame extracted from the video at the requested original-video timestamp |
|
|
168
169
|
|
|
169
|
-
These endpoints follow the same access model as `/api/public-recording
|
|
170
|
+
These endpoints follow the same access model as `/api/public-recording`, plus a
|
|
171
|
+
temporary agent-link path:
|
|
170
172
|
|
|
171
173
|
- Non-public clips return not found to anonymous callers.
|
|
174
|
+
- `create-recording-agent-link` resolves normal recording access, rejects
|
|
175
|
+
archived or trashed recordings, then mints a two-hour `agent_access` URL for
|
|
176
|
+
`/share/:recordingId`. The share page SSR advertises the agent context URL,
|
|
177
|
+
and the JSON endpoints accept the same scoped token.
|
|
172
178
|
- Expired clips return expired.
|
|
173
179
|
- Password-protected clips require `password=<pw>` once; successful JSON
|
|
174
180
|
responses include short-lived tokenized links so the plaintext password is not
|
|
@@ -183,11 +189,11 @@ These endpoints follow the same access model as `/api/public-recording`:
|
|
|
183
189
|
- Frame extraction must use the checked recording media path and must not expose
|
|
184
190
|
raw provider URLs.
|
|
185
191
|
|
|
186
|
-
The share popover's "Share with agents" field should copy
|
|
187
|
-
not raw transcript text. Its "Copy agent prompt"
|
|
188
|
-
instructions to fetch transcripts, frames, and
|
|
189
|
-
still point agents at the context response so
|
|
190
|
-
context they need.
|
|
192
|
+
The share popover's "Share with agents" field should copy an agent context URL
|
|
193
|
+
or tokenized share page URL, not raw transcript text. Its "Copy agent prompt"
|
|
194
|
+
field may wrap that URL with instructions to fetch transcripts, frames, and
|
|
195
|
+
browser diagnostics, but it should still point agents at the context response so
|
|
196
|
+
they can fetch only the visual context they need.
|
|
191
197
|
|
|
192
198
|
## View counting
|
|
193
199
|
|
|
@@ -132,8 +132,9 @@ Detailed media, meeting, dictation, editing, and sharing rules live in
|
|
|
132
132
|
- `navigate` moves the UI to recording/library/meeting/share surfaces.
|
|
133
133
|
- Use data actions for full transcripts and media metadata.
|
|
134
134
|
- For the in-app Clips agent, prefer `get-recording-player-data` for full
|
|
135
|
-
private/authenticated recording context.
|
|
136
|
-
|
|
135
|
+
private/authenticated recording context. When preparing a link for another
|
|
136
|
+
agent outside Clips, use `create-recording-agent-link`; it mints a two-hour
|
|
137
|
+
`agent_access` share URL without changing recording visibility.
|
|
137
138
|
|
|
138
139
|
## Skills
|
|
139
140
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import {
|
|
3
|
+
getRequestContext,
|
|
4
|
+
getRequestUserEmail,
|
|
5
|
+
signShortLivedToken,
|
|
6
|
+
} from "@agent-native/core/server";
|
|
7
|
+
import { ForbiddenError, resolveAccess } from "@agent-native/core/sharing";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
CLIPS_AGENT_ACCESS_PARAM,
|
|
12
|
+
CLIPS_AGENT_ACCESS_TTL_SECONDS,
|
|
13
|
+
getServerAppBasePath,
|
|
14
|
+
} from "../server/lib/public-agent-context.js";
|
|
15
|
+
import {
|
|
16
|
+
agentAccessTokenResourceId,
|
|
17
|
+
buildAgentApiUrls,
|
|
18
|
+
} from "../shared/agent-context.js";
|
|
19
|
+
|
|
20
|
+
function appOrigin(): string {
|
|
21
|
+
const origin =
|
|
22
|
+
getRequestContext()?.requestOrigin ||
|
|
23
|
+
process.env.APP_URL ||
|
|
24
|
+
process.env.BETTER_AUTH_URL ||
|
|
25
|
+
"http://localhost:3000";
|
|
26
|
+
try {
|
|
27
|
+
return new URL(origin).origin;
|
|
28
|
+
} catch {
|
|
29
|
+
return "http://localhost:3000";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function appendAgentAccess(url: string, token: string): string {
|
|
34
|
+
const sep = url.includes("?") ? "&" : "?";
|
|
35
|
+
return `${url}${sep}${CLIPS_AGENT_ACCESS_PARAM}=${encodeURIComponent(token)}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default defineAction({
|
|
39
|
+
description:
|
|
40
|
+
"Create a temporary private agent-readable link for one Clips recording. The URL is scoped to that recording and expires after two hours.",
|
|
41
|
+
schema: z.object({
|
|
42
|
+
recordingId: z.string().describe("Recording ID"),
|
|
43
|
+
}),
|
|
44
|
+
readOnly: true,
|
|
45
|
+
run: async (args) => {
|
|
46
|
+
const access = await resolveAccess("recording", args.recordingId);
|
|
47
|
+
if (!access) {
|
|
48
|
+
throw new ForbiddenError(`No access to recording ${args.recordingId}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const recording = access.resource as {
|
|
52
|
+
id: string;
|
|
53
|
+
archivedAt?: string | null;
|
|
54
|
+
trashedAt?: string | null;
|
|
55
|
+
};
|
|
56
|
+
if (recording.archivedAt || recording.trashedAt) {
|
|
57
|
+
throw new ForbiddenError(
|
|
58
|
+
`Recording ${args.recordingId} is not shareable`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const token = signShortLivedToken({
|
|
63
|
+
resourceId: agentAccessTokenResourceId(recording.id),
|
|
64
|
+
viewerEmail: getRequestUserEmail() || undefined,
|
|
65
|
+
ttlSeconds: CLIPS_AGENT_ACCESS_TTL_SECONDS,
|
|
66
|
+
});
|
|
67
|
+
const origin = appOrigin();
|
|
68
|
+
const basePath = getServerAppBasePath();
|
|
69
|
+
const expiresAt = new Date(
|
|
70
|
+
Date.now() + CLIPS_AGENT_ACCESS_TTL_SECONDS * 1000,
|
|
71
|
+
).toISOString();
|
|
72
|
+
const pageUrl = appendAgentAccess(
|
|
73
|
+
`${origin}${basePath}/share/${encodeURIComponent(recording.id)}`,
|
|
74
|
+
token,
|
|
75
|
+
);
|
|
76
|
+
const api = buildAgentApiUrls(recording.id, {
|
|
77
|
+
origin,
|
|
78
|
+
basePath,
|
|
79
|
+
token,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
recordingId: recording.id,
|
|
84
|
+
url: pageUrl,
|
|
85
|
+
contextUrl: api.contextUrl,
|
|
86
|
+
expiresAt,
|
|
87
|
+
ttlSeconds: CLIPS_AGENT_ACCESS_TTL_SECONDS,
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
appBasePath,
|
|
3
2
|
appPath,
|
|
3
|
+
useActionMutation,
|
|
4
4
|
useActionQuery,
|
|
5
5
|
useSession,
|
|
6
6
|
useT,
|
|
@@ -38,7 +38,6 @@ import {
|
|
|
38
38
|
import { Switch } from "@/components/ui/switch";
|
|
39
39
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
|
40
40
|
|
|
41
|
-
import { buildAgentApiUrls } from "../../../shared/agent-context";
|
|
42
41
|
import { isLoomEmbedUrl } from "../../../shared/loom";
|
|
43
42
|
import { withShareAttribution } from "../../../shared/share-attribution";
|
|
44
43
|
|
|
@@ -284,46 +283,27 @@ function LinkTab({
|
|
|
284
283
|
(data?.visibility as Visibility | null) ?? "private";
|
|
285
284
|
const isPublic = visibility === "public";
|
|
286
285
|
const isLoomRecording = isLoomRecordingProp || isLoomEmbedUrl(videoUrl);
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
origin: window.location.origin,
|
|
292
|
-
basePath: appBasePath(),
|
|
293
|
-
}).contextUrl;
|
|
294
|
-
const [tokenizedAgentContextUrl, setTokenizedAgentContextUrl] = useState("");
|
|
286
|
+
const createAgentLink = useActionMutation(
|
|
287
|
+
"create-recording-agent-link" as any,
|
|
288
|
+
);
|
|
289
|
+
const [agentContextUrl, setAgentContextUrl] = useState("");
|
|
295
290
|
|
|
296
291
|
useEffect(() => {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
? payload.apis.context.url
|
|
313
|
-
: "";
|
|
314
|
-
if (!cancelled) setTokenizedAgentContextUrl(contextUrl);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
void loadTokenizedAgentContextUrl();
|
|
318
|
-
return () => {
|
|
319
|
-
cancelled = true;
|
|
320
|
-
};
|
|
321
|
-
}, [hasPassword, isPublic, publicAgentContextUrl]);
|
|
322
|
-
|
|
323
|
-
const agentContextUrl = hasPassword
|
|
324
|
-
? tokenizedAgentContextUrl
|
|
325
|
-
: publicAgentContextUrl;
|
|
326
|
-
const agentShareDisabled = isPending || !isPublic || !agentContextUrl;
|
|
292
|
+
setAgentContextUrl("");
|
|
293
|
+
}, [recordingId, visibility]);
|
|
294
|
+
|
|
295
|
+
async function handleCreateAgentLink() {
|
|
296
|
+
if (createAgentLink.isPending) return;
|
|
297
|
+
const result = (await createAgentLink.mutateAsync({
|
|
298
|
+
recordingId,
|
|
299
|
+
})) as { url?: string };
|
|
300
|
+
if (!result?.url) return;
|
|
301
|
+
setAgentContextUrl(result.url);
|
|
302
|
+
copyToClipboard(result.url);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const agentShareDisabled =
|
|
306
|
+
isPending || createAgentLink.isPending || !agentContextUrl;
|
|
327
307
|
const agentPrompt = agentContextUrl
|
|
328
308
|
? t("shareDialog.agentPrompt", { agentContextUrl })
|
|
329
309
|
: "";
|
|
@@ -348,11 +328,28 @@ function LinkTab({
|
|
|
348
328
|
(and a connect link) instead of leaving it buried in Settings. */}
|
|
349
329
|
{isPublic ? <SlackShareHint canManage={canManage} /> : null}
|
|
350
330
|
|
|
351
|
-
<
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
331
|
+
<div className="space-y-2">
|
|
332
|
+
<div className="flex items-end justify-between gap-2">
|
|
333
|
+
<div className="text-xs font-medium text-muted-foreground">
|
|
334
|
+
{t("shareDialog.shareWithAgents")}
|
|
335
|
+
</div>
|
|
336
|
+
<Button
|
|
337
|
+
type="button"
|
|
338
|
+
variant="outline"
|
|
339
|
+
size="sm"
|
|
340
|
+
className="h-7"
|
|
341
|
+
onClick={() => void handleCreateAgentLink()}
|
|
342
|
+
disabled={isPending || createAgentLink.isPending}
|
|
343
|
+
>
|
|
344
|
+
{t("shareUi.copy")}
|
|
345
|
+
</Button>
|
|
346
|
+
</div>
|
|
347
|
+
<CopyField
|
|
348
|
+
label=""
|
|
349
|
+
value={agentContextUrl}
|
|
350
|
+
disabled={agentShareDisabled}
|
|
351
|
+
/>
|
|
352
|
+
</div>
|
|
356
353
|
|
|
357
354
|
<CopyField
|
|
358
355
|
label={t("shareDialog.copyAgentPrompt")}
|
|
@@ -360,7 +357,7 @@ function LinkTab({
|
|
|
360
357
|
disabled={agentShareDisabled}
|
|
361
358
|
/>
|
|
362
359
|
|
|
363
|
-
{
|
|
360
|
+
{agentContextUrl || hasPassword || !isPublic ? (
|
|
364
361
|
<p className="text-xs text-muted-foreground">
|
|
365
362
|
{t("shareDialog.agentTokenDescription")}
|
|
366
363
|
</p>
|
|
@@ -270,9 +270,11 @@ export function CopyField({
|
|
|
270
270
|
};
|
|
271
271
|
return (
|
|
272
272
|
<div>
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
273
|
+
{label ? (
|
|
274
|
+
<div className="mb-1 text-xs font-medium text-muted-foreground">
|
|
275
|
+
{label}
|
|
276
|
+
</div>
|
|
277
|
+
) : null}
|
|
276
278
|
<div className="flex items-stretch gap-2">
|
|
277
279
|
<Input
|
|
278
280
|
readOnly
|
|
@@ -419,7 +419,7 @@ const messages = {
|
|
|
419
419
|
agentPrompt:
|
|
420
420
|
"اجلب عنوان URL لسياق وكيل Clips هذا: {{agentContextUrl}}. استخدم transcript.segments للسياق المنطوق، واجلب recommendedFrames أو عناوين URL الخاصة بواجهة API للإطارات لرؤية الشاشة، وتحقق من browserDiagnostics إن وجدت لسجلات وحدة التحكم المنقحة وبيانات طلبات fetch/XHR الوصفية.",
|
|
421
421
|
agentTokenDescription:
|
|
422
|
-
"
|
|
422
|
+
"يسمح URL المؤقت هذا للوكلاء بقراءة المقطع دون جعله عاما. تنتهي صلاحيته بعد ساعتين.",
|
|
423
423
|
gifPreview: "معاينة GIF",
|
|
424
424
|
openPlayer: "مشغل مفتوح",
|
|
425
425
|
downloadMp4: "تحميل MP4",
|
|
@@ -436,7 +436,7 @@ const messages = {
|
|
|
436
436
|
agentPrompt:
|
|
437
437
|
"Rufe diese Clips-Agent-Kontext-URL ab: {{agentContextUrl}}. Verwende transcript.segments fuer den gesprochenen Kontext, rufe recommendedFrames oder die Frame-API-URLs ab, um den Bildschirm zu sehen, und pruefe browserDiagnostics, falls vorhanden, fuer redigierte Konsolenprotokolle und fetch/XHR-Anfragemetadaten.",
|
|
438
438
|
agentTokenDescription:
|
|
439
|
-
"
|
|
439
|
+
"Diese temporare Agent-URL lasst Agenten den Clip lesen, ohne ihn offentlich zu machen. Sie lauft nach zwei Stunden ab.",
|
|
440
440
|
gifPreview: "GIF-Vorschau",
|
|
441
441
|
openPlayer: "Spieler öffnen",
|
|
442
442
|
downloadMp4: "Laden Sie MP4 herunter",
|
|
@@ -419,7 +419,7 @@ const messages = {
|
|
|
419
419
|
agentPrompt:
|
|
420
420
|
"Fetch this Clips agent context URL: {{agentContextUrl}}. Use transcript.segments for spoken context, fetch recommendedFrames or the frame API URLs to see the screen, and check browserDiagnostics if present for redacted console logs and fetch/XHR request metadata.",
|
|
421
421
|
agentTokenDescription:
|
|
422
|
-
"This agent URL
|
|
422
|
+
"This temporary agent URL lets agents read the clip without making it public. It expires after two hours.",
|
|
423
423
|
gifPreview: "GIF preview",
|
|
424
424
|
openPlayer: "Open player",
|
|
425
425
|
downloadMp4: "Download MP4",
|
|
@@ -431,7 +431,7 @@ const messages = {
|
|
|
431
431
|
agentPrompt:
|
|
432
432
|
"Obtén esta URL de contexto para agentes de Clips: {{agentContextUrl}}. Usa transcript.segments para el contexto hablado, obtén recommendedFrames o las URLs de la API de fotogramas para ver la pantalla y revisa browserDiagnostics si está presente para ver registros de consola redactados y metadatos de solicitudes fetch/XHR.",
|
|
433
433
|
agentTokenDescription:
|
|
434
|
-
"
|
|
434
|
+
"Esta URL temporal para agentes permite leer el clip sin hacerlo publico. Caduca en dos horas.",
|
|
435
435
|
gifPreview: "vista previa de GIF",
|
|
436
436
|
openPlayer: "jugador abierto",
|
|
437
437
|
downloadMp4: "Descargar MP4",
|
|
@@ -431,7 +431,7 @@ const messages = {
|
|
|
431
431
|
agentPrompt:
|
|
432
432
|
"Récupère cette URL de contexte Clips pour agent : {{agentContextUrl}}. Utilise transcript.segments pour le contexte parlé, récupère recommendedFrames ou les URLs de l'API d'images pour voir l'écran, et consulte browserDiagnostics s'il est présent pour les journaux de console expurgés et les métadonnées de requêtes fetch/XHR.",
|
|
433
433
|
agentTokenDescription:
|
|
434
|
-
"
|
|
434
|
+
"Cette URL temporaire pour agents permet de lire le clip sans le rendre public. Elle expire dans deux heures.",
|
|
435
435
|
gifPreview: "aperçu de GIF",
|
|
436
436
|
openPlayer: "Joueur ouvert",
|
|
437
437
|
downloadMp4: "Télécharger MP4",
|
|
@@ -416,7 +416,7 @@ const messages = {
|
|
|
416
416
|
agentPrompt:
|
|
417
417
|
"यह Clips एजेंट संदर्भ URL प्राप्त करें: {{agentContextUrl}}। बोले गए संदर्भ के लिए transcript.segments का उपयोग करें, स्क्रीन देखने के लिए recommendedFrames या फ्रेम API URL प्राप्त करें, और यदि browserDiagnostics मौजूद हो तो संशोधित कंसोल लॉग और fetch/XHR अनुरोध मेटाडेटा जांचें।",
|
|
418
418
|
agentTokenDescription:
|
|
419
|
-
"यह एजेंट URL
|
|
419
|
+
"यह अस्थायी एजेंट URL क्लिप को सार्वजनिक किए बिना एजेंटों को इसे पढ़ने देता है। यह दो घंटे बाद समाप्त हो जाता है।",
|
|
420
420
|
gifPreview: "GIF पूर्वावलोकन",
|
|
421
421
|
openPlayer: "खुला खिलाड़ी",
|
|
422
422
|
downloadMp4: "MP4 डाउनलोड करें",
|
|
@@ -428,7 +428,7 @@ const messages = {
|
|
|
428
428
|
agentPrompt:
|
|
429
429
|
"この Clips エージェントコンテキスト URL を取得してください: {{agentContextUrl}}。音声の文脈には transcript.segments を使い、画面を見るために recommendedFrames またはフレーム API URL を取得し、browserDiagnostics がある場合は、編集済みのコンソールログと fetch/XHR リクエストのメタデータを確認してください。",
|
|
430
430
|
agentTokenDescription:
|
|
431
|
-
"
|
|
431
|
+
"この一時的なエージェント URL により、クリップを公開せずにエージェントが読み取れます。2 時間後に期限切れになります。",
|
|
432
432
|
gifPreview: "GIF プレビュー",
|
|
433
433
|
openPlayer: "プレーヤーを開く",
|
|
434
434
|
downloadMp4: "ダウンロード",
|
|
@@ -421,7 +421,7 @@ const messages = {
|
|
|
421
421
|
agentPrompt:
|
|
422
422
|
"이 Clips 에이전트 컨텍스트 URL을 가져오세요: {{agentContextUrl}}. 말한 내용의 맥락은 transcript.segments를 사용하고, 화면을 보기 위해 recommendedFrames 또는 프레임 API URL을 가져오며, browserDiagnostics가 있으면 수정된 콘솔 로그와 fetch/XHR 요청 메타데이터를 확인하세요.",
|
|
423
423
|
agentTokenDescription:
|
|
424
|
-
"이 에이전트 URL
|
|
424
|
+
"이 임시 에이전트 URL을 사용하면 클립을 공개하지 않고도 에이전트가 읽을 수 있습니다. 2시간 후 만료됩니다.",
|
|
425
425
|
gifPreview: "GIF 미리보기",
|
|
426
426
|
openPlayer: "플레이어 열기",
|
|
427
427
|
downloadMp4: "MP4 다운로드",
|
|
@@ -429,7 +429,7 @@ const messages = {
|
|
|
429
429
|
agentPrompt:
|
|
430
430
|
"Busque esta URL de contexto para agentes do Clips: {{agentContextUrl}}. Use transcript.segments para o contexto falado, busque recommendedFrames ou as URLs da API de quadros para ver a tela e confira browserDiagnostics, se presente, para logs de console redigidos e metadados de solicitações fetch/XHR.",
|
|
431
431
|
agentTokenDescription:
|
|
432
|
-
"
|
|
432
|
+
"Esta URL temporaria para agentes permite ler o clipe sem torna-lo publico. Ela expira em duas horas.",
|
|
433
433
|
gifPreview: "visualização de GIF",
|
|
434
434
|
openPlayer: "Jogador aberto",
|
|
435
435
|
downloadMp4: "Baixar MP4",
|