create-devoplus-one-app 0.0.0-stage → 0.1.0-next.158
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 +62 -2
- package/dist/cli.mjs +17 -0
- package/package.json +35 -3
- package/templates/0001_init.sql.tpl +10 -0
- package/templates/AGENTS.md.tpl +286 -0
- package/templates/AGENT_GUIDE.md.tpl +36 -0
- package/templates/README.md.tpl +58 -0
- package/templates/_shared/AGENTS.writes.md.tpl +38 -0
- package/templates/_shared/README.tail.md.tpl +20 -0
- package/templates/_shared/files/src/app.test.ts.tpl +36 -0
- package/templates/_shared/files/src/index.ts.tpl +8 -0
- package/templates/_shared/files/src/lib/client.ts.tpl +213 -0
- package/templates/_shared/files/src/lib/files.ts.tpl +37 -0
- package/templates/_shared/files/src/lib/http.ts.tpl +101 -0
- package/templates/_shared/files/src/lib/mail.ts.tpl +26 -0
- package/templates/_shared/files/src/lib/model.ts.tpl +31 -0
- package/templates/_shared/files/src/lib/page.ts.tpl +71 -0
- package/templates/_shared/files/src/lib/record.ts.tpl +246 -0
- package/templates/_shared/files/src/lib/server.ts.tpl +132 -0
- package/templates/_shared/files/src/shell.test.ts.tpl +56 -0
- package/templates/agent.ts.tpl +23 -0
- package/templates/bakim-takvimi/AGENTS.template.md.tpl +40 -0
- package/templates/bakim-takvimi/README.md.tpl +45 -0
- package/templates/bakim-takvimi/files/AGENT_GUIDE.md.tpl +52 -0
- package/templates/bakim-takvimi/files/one.data.json +45 -0
- package/templates/bakim-takvimi/files/src/agent.ts.tpl +22 -0
- package/templates/bakim-takvimi/files/src/app.ts.tpl +259 -0
- package/templates/bakim-takvimi/files/src/jobs.ts.tpl +170 -0
- package/templates/bakim-takvimi/files/src/names.ts.tpl +5 -0
- package/templates/bakim-takvimi/files/src/queries.ts.tpl +71 -0
- package/templates/bakim-takvimi/files/src/schedule.test.ts.tpl +58 -0
- package/templates/bakim-takvimi/files/src/schedule.ts.tpl +106 -0
- package/templates/bakim-takvimi/files/src/shell.ts.tpl +299 -0
- package/templates/bakim-takvimi/manifest.json.tpl +64 -0
- package/templates/egitim-sertifika/AGENTS.template.md.tpl +41 -0
- package/templates/egitim-sertifika/README.md.tpl +46 -0
- package/templates/egitim-sertifika/files/AGENT_GUIDE.md.tpl +53 -0
- package/templates/egitim-sertifika/files/one.data.json +43 -0
- package/templates/egitim-sertifika/files/src/agent.ts.tpl +22 -0
- package/templates/egitim-sertifika/files/src/app.ts.tpl +312 -0
- package/templates/egitim-sertifika/files/src/certificate.test.ts.tpl +83 -0
- package/templates/egitim-sertifika/files/src/certificate.ts.tpl +163 -0
- package/templates/egitim-sertifika/files/src/jobs.ts.tpl +219 -0
- package/templates/egitim-sertifika/files/src/names.ts.tpl +5 -0
- package/templates/egitim-sertifika/files/src/queries.ts.tpl +115 -0
- package/templates/egitim-sertifika/files/src/shell.ts.tpl +345 -0
- package/templates/egitim-sertifika/manifest.json.tpl +57 -0
- package/templates/envanter/AGENTS.template.md.tpl +28 -0
- package/templates/envanter/README.md.tpl +40 -0
- package/templates/envanter/files/AGENT_GUIDE.md.tpl +47 -0
- package/templates/envanter/files/one.data.json +62 -0
- package/templates/envanter/files/src/agent.ts.tpl +20 -0
- package/templates/envanter/files/src/app.ts.tpl +204 -0
- package/templates/envanter/files/src/jobs.ts.tpl +30 -0
- package/templates/envanter/files/src/queries.ts.tpl +53 -0
- package/templates/envanter/files/src/shell.ts.tpl +265 -0
- package/templates/envanter/files/src/stock.test.ts.tpl +31 -0
- package/templates/envanter/files/src/stock.ts.tpl +33 -0
- package/templates/envanter/manifest.json.tpl +56 -0
- package/templates/hedef-takibi/AGENTS.template.md.tpl +37 -0
- package/templates/hedef-takibi/README.md.tpl +52 -0
- package/templates/hedef-takibi/files/AGENT_GUIDE.md.tpl +48 -0
- package/templates/hedef-takibi/files/one.data.json +76 -0
- package/templates/hedef-takibi/files/src/agent.ts.tpl +26 -0
- package/templates/hedef-takibi/files/src/app.ts.tpl +740 -0
- package/templates/hedef-takibi/files/src/progress.test.ts.tpl +53 -0
- package/templates/hedef-takibi/files/src/progress.ts.tpl +81 -0
- package/templates/hedef-takibi/files/src/queries.ts.tpl +83 -0
- package/templates/hedef-takibi/files/src/shell.ts.tpl +455 -0
- package/templates/hedef-takibi/manifest.json.tpl +38 -0
- package/templates/index.ts.tpl +262 -0
- package/templates/markdown.d.ts.tpl +6 -0
- package/templates/musteri-ziyaret/AGENTS.template.md.tpl +47 -0
- package/templates/musteri-ziyaret/README.md.tpl +48 -0
- package/templates/musteri-ziyaret/files/AGENT_GUIDE.md.tpl +44 -0
- package/templates/musteri-ziyaret/files/one.data.json +53 -0
- package/templates/musteri-ziyaret/files/src/agent.ts.tpl +21 -0
- package/templates/musteri-ziyaret/files/src/app.ts.tpl +340 -0
- package/templates/musteri-ziyaret/files/src/customers.ts.tpl +85 -0
- package/templates/musteri-ziyaret/files/src/queries.ts.tpl +67 -0
- package/templates/musteri-ziyaret/files/src/shell.ts.tpl +272 -0
- package/templates/musteri-ziyaret/files/src/visit.test.ts.tpl +65 -0
- package/templates/musteri-ziyaret/files/src/visit.ts.tpl +98 -0
- package/templates/musteri-ziyaret/manifest.json.tpl +37 -0
- package/templates/saha-denetimi/AGENTS.template.md.tpl +32 -0
- package/templates/saha-denetimi/README.md.tpl +44 -0
- package/templates/saha-denetimi/files/AGENT_GUIDE.md.tpl +55 -0
- package/templates/saha-denetimi/files/one.data.json +87 -0
- package/templates/saha-denetimi/files/src/agent.ts.tpl +41 -0
- package/templates/saha-denetimi/files/src/app.ts.tpl +444 -0
- package/templates/saha-denetimi/files/src/queries.ts.tpl +143 -0
- package/templates/saha-denetimi/files/src/score.test.ts.tpl +48 -0
- package/templates/saha-denetimi/files/src/score.ts.tpl +102 -0
- package/templates/saha-denetimi/files/src/shell.ts.tpl +585 -0
- package/templates/saha-denetimi/manifest.json.tpl +49 -0
- package/templates/shell.test.ts.tpl +30 -0
- package/templates/shell.ts.tpl +201 -0
- package/templates/uygunsuzluk-dof/AGENTS.template.md.tpl +40 -0
- package/templates/uygunsuzluk-dof/README.md.tpl +46 -0
- package/templates/uygunsuzluk-dof/files/AGENT_GUIDE.md.tpl +56 -0
- package/templates/uygunsuzluk-dof/files/one.data.json +99 -0
- package/templates/uygunsuzluk-dof/files/src/agent.ts.tpl +45 -0
- package/templates/uygunsuzluk-dof/files/src/app.ts.tpl +431 -0
- package/templates/uygunsuzluk-dof/files/src/capa.test.ts.tpl +53 -0
- package/templates/uygunsuzluk-dof/files/src/capa.ts.tpl +114 -0
- package/templates/uygunsuzluk-dof/files/src/jobs.ts.tpl +127 -0
- package/templates/uygunsuzluk-dof/files/src/queries.ts.tpl +102 -0
- package/templates/uygunsuzluk-dof/files/src/shell.ts.tpl +441 -0
- package/templates/uygunsuzluk-dof/manifest.json.tpl +56 -0
- package/templates/ziyaretci-kaydi/AGENTS.template.md.tpl +35 -0
- package/templates/ziyaretci-kaydi/README.md.tpl +46 -0
- package/templates/ziyaretci-kaydi/files/AGENT_GUIDE.md.tpl +39 -0
- package/templates/ziyaretci-kaydi/files/one.data.json +42 -0
- package/templates/ziyaretci-kaydi/files/src/agent.ts.tpl +13 -0
- package/templates/ziyaretci-kaydi/files/src/app.ts.tpl +293 -0
- package/templates/ziyaretci-kaydi/files/src/jobs.ts.tpl +49 -0
- package/templates/ziyaretci-kaydi/files/src/notice.ts.tpl +48 -0
- package/templates/ziyaretci-kaydi/files/src/queries.ts.tpl +45 -0
- package/templates/ziyaretci-kaydi/files/src/shell.ts.tpl +252 -0
- package/templates/ziyaretci-kaydi/files/src/visitor.test.ts.tpl +34 -0
- package/templates/ziyaretci-kaydi/files/src/visitor.ts.tpl +57 -0
- package/templates/ziyaretci-kaydi/manifest.json.tpl +38 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/// <reference path="./markdown.d.ts" />
|
|
2
|
+
// {{nameTr}} — Devoplus One uygulaması (iskelet: create-devoplus-one-app). Uçlar:
|
|
3
|
+
// GET / kabuk (sayfa; manifest `navigation` yolları da kabuğu döndürür — sekmeleri Portal çizer)
|
|
4
|
+
// GET /shell.js kabuğun betiği (satır içi betik platformda kapalıdır)
|
|
5
|
+
// GET /one-bridge.js · /one-ui.css Devoplus One köprüsü ve görsel dili (SDK clientAssetResponse)
|
|
6
|
+
// GET /api/me oturumdaki kullanıcı: dispatch bağlamı + CA Users/me
|
|
7
|
+
// GET /api/notes bu kurulumun veritabanındaki notlarım (PortalId kolonu YOK — veritabanı zaten bu portala aittir, D-17)
|
|
8
|
+
// POST /api/notes not ekle { text }
|
|
9
|
+
// DELETE /api/notes/{id} notumu sil
|
|
10
|
+
{{#files}}
|
|
11
|
+
// GET/POST /api/files · GET/DELETE /api/files/{key} bu kurulumun dosya deposu (SDK OneFiles; env.FILES) — kullanıcı yalnız kendi dosyalarını görür
|
|
12
|
+
{{/files}}
|
|
13
|
+
{{#cache}}
|
|
14
|
+
// GET/PUT/DELETE /api/cache bu kurulumun önbelleği (SDK OneCache; env.CACHE)
|
|
15
|
+
{{/cache}}
|
|
16
|
+
// POST /__one/agent/list-notes Devoplus yapay zekâsının aracı — YALNIZ platformun imzalı yolundan (src/agent.ts)
|
|
17
|
+
// GET /.well-known/one-agent.json · one-agent.md yapay zekâ kataloğu (manifest `agent`) ve rehberi (AGENT_GUIDE.md)
|
|
18
|
+
// Kimlik X-One-* başlıklarından OKUNUR (dispatch yazar); bu dosya o başlıkları asla üretmez.
|
|
19
|
+
import {
|
|
20
|
+
BackendClient,
|
|
21
|
+
agentManifestResponse,
|
|
22
|
+
clientAssetResponse,
|
|
23
|
+
handleAgentRequest,
|
|
24
|
+
navigationFromManifest,
|
|
25
|
+
scriptResponse,
|
|
26
|
+
{{#cache}}
|
|
27
|
+
OneCache,
|
|
28
|
+
{{/cache}}
|
|
29
|
+
{{#files}}
|
|
30
|
+
OneFiles,
|
|
31
|
+
{{/files}}
|
|
32
|
+
logAudit,
|
|
33
|
+
problemResponse,
|
|
34
|
+
readBoundedBody,
|
|
35
|
+
readContext,
|
|
36
|
+
requireUser,
|
|
37
|
+
{{#files}}
|
|
38
|
+
sanitizeFileName,
|
|
39
|
+
{{/files}}
|
|
40
|
+
type OneApp,
|
|
41
|
+
{{#cache}}
|
|
42
|
+
type OneCacheBinding,
|
|
43
|
+
{{/cache}}
|
|
44
|
+
type OneContext,
|
|
45
|
+
type OneDatabase,
|
|
46
|
+
{{#files}}
|
|
47
|
+
type OneFilesBinding,
|
|
48
|
+
{{/files}}
|
|
49
|
+
} from '@devoplus/one-sdk'
|
|
50
|
+
import manifest from '../one.manifest.json' with { type: 'json' }
|
|
51
|
+
import guide from '../AGENT_GUIDE.md' with { type: 'text' }
|
|
52
|
+
import { agentTools } from './agent.ts'
|
|
53
|
+
import { normalizeLang, normalizeTheme, renderShell, renderShellScript } from './shell.ts'
|
|
54
|
+
|
|
55
|
+
// Menü manifest'ten gelir (tek kaynak): Portal sekmeleri çizer, sayfa betiği bir kez üretilir.
|
|
56
|
+
const NAVIGATION = navigationFromManifest(manifest)
|
|
57
|
+
// Salt-okunur uyarısı manifestin Vekalet / Yerine Görüntüle kipine uyar (actingSessions: "write" ise gösterilmez).
|
|
58
|
+
const SHELL_SCRIPT = renderShellScript(NAVIGATION, {
|
|
59
|
+
actingWrites: (manifest as { actingSessions?: string }).actingSessions === 'write',
|
|
60
|
+
})
|
|
61
|
+
const isPage = (pathname: string): boolean => pathname === '/' || NAVIGATION.some((item) => item.path === pathname)
|
|
62
|
+
|
|
63
|
+
interface Env {
|
|
64
|
+
/** Bu kurulumun veritabanı — yalnız bu portala aittir (D-17); tabloya PortalId eklenmez. */
|
|
65
|
+
DB: OneDatabase
|
|
66
|
+
{{#files}}
|
|
67
|
+
/** Bu kurulumun dosya deposu (manifest services.files → env.FILES). */
|
|
68
|
+
FILES: OneFilesBinding
|
|
69
|
+
{{/files}}
|
|
70
|
+
{{#cache}}
|
|
71
|
+
/** Bu kurulumun önbelleği (manifest services.cache → env.CACHE). TTL zorunludur; kalıcı veri için DEĞİL. */
|
|
72
|
+
CACHE: OneCacheBinding
|
|
73
|
+
{{/cache}}
|
|
74
|
+
/** CA backend tabanı; one-ci `vars` olarak yazar ve outbound allowlist'indedir. */
|
|
75
|
+
API_BASE: string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface MeResponse {
|
|
79
|
+
id: string
|
|
80
|
+
firstName?: string
|
|
81
|
+
lastName?: string
|
|
82
|
+
email?: string
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface NoteRow {
|
|
86
|
+
id: number
|
|
87
|
+
text: string
|
|
88
|
+
createdAt: string
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const NOTE_MAX_LENGTH = 500
|
|
92
|
+
const NOTE_BODY_MAX_BYTES = 16 * 1024
|
|
93
|
+
{{#files}}
|
|
94
|
+
const UPLOAD_PREFIX = 'uploads/'
|
|
95
|
+
const UPLOAD_MAX_BYTES = 5 * 1024 * 1024
|
|
96
|
+
// Yüklenen anahtar `<ön ek>/<uuid>-<ad>`: indirme adı için uuid ön eki soyulur.
|
|
97
|
+
const UPLOAD_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-/
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Her kullanıcının dosyaları KENDİ ön ekindedir (`uploads/<sub>/`); liste, indirme ve silme bu ön ekin dışına
|
|
101
|
+
* çıkamaz. Tek bir ortak ön ek kullanılsaydı aynı adla yüklenen dosya bir başkasınınkini EZERDİ ve anahtarı bilen
|
|
102
|
+
* herkes silebilirdi. `sub` URL kodlanır ki anahtar parçası `/` ya da kontrol karakteri taşımasın.
|
|
103
|
+
*/
|
|
104
|
+
function uploadPrefixFor(sub: string): string {
|
|
105
|
+
return `${UPLOAD_PREFIX}${encodeURIComponent(sub)}/`
|
|
106
|
+
}
|
|
107
|
+
{{/files}}
|
|
108
|
+
|
|
109
|
+
function json(body: unknown, status = 200): Response {
|
|
110
|
+
return Response.json(body, { status })
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** JSON gövdeyi SINIRLI okur: hazır `.json()` metodu gövdenin tamamını belleğe aldıktan SONRA ayrıştırırdı. */
|
|
114
|
+
async function readJsonBody(request: Request, maxBytes: number): Promise<unknown> {
|
|
115
|
+
const buffer = await readBoundedBody(request, maxBytes)
|
|
116
|
+
try {
|
|
117
|
+
return JSON.parse(new TextDecoder().decode(buffer))
|
|
118
|
+
} catch {
|
|
119
|
+
return null
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export default {
|
|
124
|
+
async fetch(request, env): Promise<Response> {
|
|
125
|
+
const { pathname, searchParams } = new URL(request.url)
|
|
126
|
+
const method = request.method
|
|
127
|
+
// `catch` bağlamı görsün diye dışarıda: beklenmeyen hata Event Logger'a uygulama ve portal bağlamıyla yazılır (D-45).
|
|
128
|
+
let context: OneContext | undefined
|
|
129
|
+
try {
|
|
130
|
+
// Devoplus yapay zekâsı: imzalı araç çağrısı (`/__one/agent/…`) ve katalog/rehber (`/.well-known/one-agent.*`) —
|
|
131
|
+
// SDK karşılar; yönlendirmeden ÖNCE durur.
|
|
132
|
+
const agent = (await handleAgentRequest(request, env, agentTools, manifest)) ?? agentManifestResponse(request, manifest, guide)
|
|
133
|
+
if (agent) return agent
|
|
134
|
+
|
|
135
|
+
const ctx = readContext(request.headers)
|
|
136
|
+
context = ctx
|
|
137
|
+
|
|
138
|
+
if (method === 'GET') {
|
|
139
|
+
const asset = clientAssetResponse(pathname)
|
|
140
|
+
if (asset) return asset
|
|
141
|
+
if (pathname === '/shell.js') return scriptResponse(SHELL_SCRIPT)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (method === 'GET' && isPage(pathname)) {
|
|
145
|
+
return new Response(renderShell(normalizeLang(searchParams.get('lang')), normalizeTheme(searchParams.get('theme'))), {
|
|
146
|
+
headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' },
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (pathname === '/api/me' && method === 'GET') {
|
|
151
|
+
const user = requireUser(ctx)
|
|
152
|
+
const api = new BackendClient({ baseUrl: env.API_BASE, authorization: request.headers.get('authorization') })
|
|
153
|
+
const me = await api.get<MeResponse>('Users/me')
|
|
154
|
+
return json({ app: ctx.app, portalKey: ctx.portalKey, sub: user.sub, scope: user.scope, me })
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── Notlar: bu kurulumun KENDİ veritabanı. Satırlar `sub` ile kişiye bağlıdır; portal süzgeci GEREKMEZ (veritabanı zaten bu portalın). ──
|
|
158
|
+
if (pathname === '/api/notes' && method === 'GET') {
|
|
159
|
+
const user = requireUser(ctx)
|
|
160
|
+
const rows = await env.DB.prepare('SELECT id, text, created_at AS createdAt FROM notes WHERE sub = ? ORDER BY id DESC LIMIT 50')
|
|
161
|
+
.bind(user.sub)
|
|
162
|
+
.all<NoteRow>()
|
|
163
|
+
return json({ notes: rows.results })
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (pathname === '/api/notes' && method === 'POST') {
|
|
167
|
+
const user = requireUser(ctx)
|
|
168
|
+
const body = (await readJsonBody(request, NOTE_BODY_MAX_BYTES)) as { text?: unknown } | null
|
|
169
|
+
const text = typeof body?.text === 'string' ? body.text.trim() : ''
|
|
170
|
+
if (text.length === 0 || text.length > NOTE_MAX_LENGTH) return json({ detail: `Not metni 1–${NOTE_MAX_LENGTH} karakter olmalı.` }, 400)
|
|
171
|
+
const result = await env.DB.prepare('INSERT INTO notes (sub, text) VALUES (?, ?)').bind(user.sub, text).run()
|
|
172
|
+
const id = Number(result.meta.last_row_id)
|
|
173
|
+
logAudit(ctx, '{{slug}}.note.create', String(id))
|
|
174
|
+
return json({ id }, 201)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (pathname.startsWith('/api/notes/') && method === 'DELETE') {
|
|
178
|
+
const user = requireUser(ctx)
|
|
179
|
+
const id = Number(pathname.slice('/api/notes/'.length))
|
|
180
|
+
if (!Number.isInteger(id) || id <= 0) return json({ detail: 'Geçersiz not kimliği.' }, 400)
|
|
181
|
+
// Yalnız kendi notu: `AND sub = ?` olmasaydı kimliği bilen başkasının notunu silerdi.
|
|
182
|
+
const result = await env.DB.prepare('DELETE FROM notes WHERE id = ? AND sub = ?').bind(id, user.sub).run()
|
|
183
|
+
if (result.meta.changes === 0) return json({ detail: 'Not bulunamadı.' }, 404)
|
|
184
|
+
logAudit(ctx, '{{slug}}.note.delete', String(id))
|
|
185
|
+
return json({ deleted: id })
|
|
186
|
+
}
|
|
187
|
+
{{#files}}
|
|
188
|
+
|
|
189
|
+
// ── Dosyalar: bu kurulumun KENDİ dosya deposu (SDK OneFiles anahtar/boyut kurallarını uygular) ──────────────
|
|
190
|
+
if (pathname === '/api/files' && method === 'GET') {
|
|
191
|
+
const user = requireUser(ctx)
|
|
192
|
+
const files = new OneFiles(env.FILES)
|
|
193
|
+
// Yalnız kendi ön eki listelenir. Liste imleci istemciye OLDUĞU GİBİ döner ve `?cursor=` ile geri alınır
|
|
194
|
+
// (sıra leksikografiktir, yükleme sırası değil).
|
|
195
|
+
const cursor = searchParams.get('cursor')
|
|
196
|
+
const page = await files.list({ prefix: uploadPrefixFor(user.sub), limit: 20, ...(cursor ? { cursor } : {}) })
|
|
197
|
+
return json({ files: page.files, truncated: page.truncated, cursor: page.cursor ?? null })
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (pathname === '/api/files' && method === 'POST') {
|
|
201
|
+
const user = requireUser(ctx)
|
|
202
|
+
const files = new OneFiles(env.FILES)
|
|
203
|
+
const name = sanitizeFileName(request.headers.get('x-file-name') ?? 'dosya.bin')
|
|
204
|
+
const contentType = request.headers.get('content-type') ?? 'application/octet-stream'
|
|
205
|
+
// Sınır gövde belleğe alınmadan ÖNCE uygulanır (Content-Length), parçalı akışta okuma sırasında kesilir.
|
|
206
|
+
const body = await readBoundedBody(request, UPLOAD_MAX_BYTES)
|
|
207
|
+
// Anahtar her yüklemede benzersizdir (uuid): aynı adla ikinci yükleme öncekini EZMEZ, iki kayıt olur.
|
|
208
|
+
const key = `${uploadPrefixFor(user.sub)}${crypto.randomUUID()}-${name}`
|
|
209
|
+
const stored = await files.put(key, body, { contentType, maxBytes: UPLOAD_MAX_BYTES, metadata: { sub: user.sub } })
|
|
210
|
+
logAudit(ctx, '{{slug}}.file.put', stored.key, { size: stored.size })
|
|
211
|
+
return json(stored, 201)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (pathname.startsWith('/api/files/') && (method === 'GET' || method === 'DELETE')) {
|
|
215
|
+
const user = requireUser(ctx)
|
|
216
|
+
const files = new OneFiles(env.FILES)
|
|
217
|
+
const key = decodeURIComponent(pathname.slice('/api/files/'.length))
|
|
218
|
+
// Başkasının ön eki 404'tür (403 değil): anahtar uzayı yoklanamaz, "var ama senin değil" sızmaz.
|
|
219
|
+
if (!key.startsWith(uploadPrefixFor(user.sub))) return json({ detail: 'Dosya bulunamadı.' }, 404)
|
|
220
|
+
if (method === 'DELETE') {
|
|
221
|
+
await files.delete(key)
|
|
222
|
+
logAudit(ctx, '{{slug}}.file.delete', key, { sub: user.sub })
|
|
223
|
+
return json({ deleted: key })
|
|
224
|
+
}
|
|
225
|
+
const object = await files.get(key)
|
|
226
|
+
if (!object) return json({ detail: 'Dosya bulunamadı.' }, 404)
|
|
227
|
+
const headers = new Headers()
|
|
228
|
+
object.writeHttpMetadata(headers)
|
|
229
|
+
headers.set('etag', object.httpEtag)
|
|
230
|
+
const downloadName = (key.split('/').pop() ?? 'dosya').replace(UPLOAD_ID_RE, '')
|
|
231
|
+
headers.set('content-disposition', `attachment; filename="${downloadName}"`)
|
|
232
|
+
return new Response(object.body, { headers })
|
|
233
|
+
}
|
|
234
|
+
{{/files}}
|
|
235
|
+
{{#cache}}
|
|
236
|
+
|
|
237
|
+
// ── Önbellek: bu kurulumun KENDİ önbelleği. TTL zorunlu; önbellek nihai tutarlıdır — sayaç/kilit/kalıcı kayıt için DEĞİL. ──
|
|
238
|
+
if (pathname === '/api/cache' && (method === 'GET' || method === 'PUT' || method === 'DELETE')) {
|
|
239
|
+
const user = requireUser(ctx)
|
|
240
|
+
const cache = new OneCache(env.CACHE)
|
|
241
|
+
const key = `me:${user.sub}`
|
|
242
|
+
if (method === 'GET') {
|
|
243
|
+
const value = await cache.get(key)
|
|
244
|
+
return json({ key, hit: value !== null, value })
|
|
245
|
+
}
|
|
246
|
+
if (method === 'DELETE') {
|
|
247
|
+
await cache.delete(key)
|
|
248
|
+
return json({ deleted: key })
|
|
249
|
+
}
|
|
250
|
+
const value = { writtenAt: new Date().toISOString(), sub: user.sub }
|
|
251
|
+
await cache.put(key, value, { ttlSeconds: 300 })
|
|
252
|
+
return json({ key, value }, 201)
|
|
253
|
+
}
|
|
254
|
+
{{/cache}}
|
|
255
|
+
|
|
256
|
+
return json({ detail: 'Bulunamadı.' }, 404)
|
|
257
|
+
} catch (error) {
|
|
258
|
+
// OneError → RFC 9457 ProblemDetails (status + code); bilinmeyen hata 500 ONE-SDK-E-999 (ayrıntı sızdırılmaz).
|
|
259
|
+
return problemResponse(error, context)
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
} satisfies OneApp<Env>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// `import guide from '../AGENT_GUIDE.md' with { type: 'text' }` — Markdown dosyası derlemede metin olarak gömülür
|
|
2
|
+
// (`one build` ve yayın aynı paketleyiciyi kullanır). Bu bildirim yalnız tip denetimi içindir.
|
|
3
|
+
declare module '*.md' {
|
|
4
|
+
const text: string
|
|
5
|
+
export default text
|
|
6
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
## Bu uygulamanın şablonu: müşteri ziyaret raporu (`musteri-ziyaret`)
|
|
2
|
+
|
|
3
|
+
Uygulama `create-devoplus-one-app --template musteri-ziyaret` ile üretildi. Kullanıcının ihtiyacını bu şablona göre
|
|
4
|
+
DEĞİŞTİRİN; baştan yazmayın. Dosya haritası:
|
|
5
|
+
|
|
6
|
+
| Dosya | Ne |
|
|
7
|
+
|---|---|
|
|
8
|
+
| `one.data.json` | Veri modeli: `visits` (müşteri görüşmesi). Değişince `pnpm data:generate` |
|
|
9
|
+
| `src/app.ts` | Uçlar: ziyaretlerim (kendi kaydım), ekip ziyaretleri (sunucuda hesaplanan ekip + Excel), tüm ziyaretler, müşteri listesi, özet |
|
|
10
|
+
| `src/visit.ts` | Saf kurallar: tarih ve sonraki adım, hafta aralığı, **ekip hesabı `teamOf` (`TEAM_DEPTH`)** + `visit.test.ts` |
|
|
11
|
+
| `src/customers.ts` | **ERP müşteri listesi — YER TUTUCU `CUSTOMER_QUERY`** (DEXA okuma sorgusu; bağlı değilse elle giriş) |
|
|
12
|
+
| `src/shell.ts` | Sayfa: ziyaretlerim (özet + form + müşteri seçici), ekip ziyaretleri, tüm ziyaretler |
|
|
13
|
+
| `src/agent.ts` · `src/queries.ts` · `AGENT_GUIDE.md` | Devoplus yapay zekâsının aracı ve rehberi |
|
|
14
|
+
| `src/lib/*` | Şablonların ortak parçaları — gerekmedikçe değiştirmeyin |
|
|
15
|
+
|
|
16
|
+
### ERP müşteri listesini bağlamak (`src/customers.ts`)
|
|
17
|
+
|
|
18
|
+
⛔ Sorgu anahtarı ve kolon adları **UYDURULMAZ**. `CUSTOMER_QUERY = { key, codeColumn, nameColumn }` üçü de boşken
|
|
19
|
+
uygulama DEXA'ya hiç gitmez, `/api/customers` `state: "not-configured"` döner ve müşteri elle girilir. Bağlamak için:
|
|
20
|
+
|
|
21
|
+
1. Kullanıcıdan ya da yöneticiden kurumun DEXA Query Store'undaki **müşteri listesi okuma sorgusunun anahtarını** ve
|
|
22
|
+
sorgunun döndürdüğü **müşteri kodu** ile **müşteri adı** kolonlarının adlarını öğrenin. Sorgu parametre almayan bir
|
|
23
|
+
liste sorgusu olmalıdır. Sorgu yoksa erişim talebiyle `new-query` isteyin (taslak SQL yalnız öneridir).
|
|
24
|
+
2. Üç değeri `CUSTOMER_QUERY`'ye yazın.
|
|
25
|
+
3. `one.manifest.json`'a `scopes` içine `"dexa.read"` ve `services` içine `"dexa": { "queryKeys": ["<anahtar>"] }`
|
|
26
|
+
ekleyin — manifest şeması ikisini birlikte ister.
|
|
27
|
+
4. Erişim talepleri: sorgunun uygulamaya açılması `dexa-query` (`queryKey`), yetki alanı `scope` (`dexa.read`).
|
|
28
|
+
|
|
29
|
+
Durumlar: `ok` · `not-configured` · `not-permitted` (yetki alanı belirteçte yok ya da sunucu 401/403 döndü) ·
|
|
30
|
+
`unavailable` (ERP'ye ulaşılamıyor; boş liste "müşteri yok" DEĞİLDİR) · `misconfigured` (yanıtta kolonlar yok). `ok`
|
|
31
|
+
dışındaki her durumda kayıt elle girilen adla yapılır ve `customer_source = "manual"` olur.
|
|
32
|
+
|
|
33
|
+
Değişmezler (bozmayın):
|
|
34
|
+
|
|
35
|
+
- **ERP'ye YAZILMAZ.** Bu şablon yalnız okur; ERP'ye yazma (`dexa.write`) istenirse bunun ayrı bir onay ve işlem
|
|
36
|
+
anahtarı gerektirdiğini söyleyin.
|
|
37
|
+
- **Müşteri kaynağını SUNUCU belirler:** ERP listesi bağlıyken kod verilen kayıtta kod listede olmalıdır (yoksa 400) ve
|
|
38
|
+
müşteri adı ERP'den alınır (`customer_source = "erp"`); istemcinin gönderdiği ad o durumda yok sayılır.
|
|
39
|
+
- **Temsilci SUNUCUDAN gelir:** `rep_org_id` ve `rep_name` `Users/me`'den yazılır, gövdede gönderilemez (400).
|
|
40
|
+
`rep_org_id` Organization Schema kimliğidir, oturum kimliği (`sub`) DEĞİLDİR.
|
|
41
|
+
- **Ekip her istekte organizasyon şemasından SUNUCUDA hesaplanır** (`getOrgUsersWithManagers` + `teamOf`, fonksiyonel
|
|
42
|
+
yönetici). Kayıtta yönetici TUTULMAZ; istemciden ekip ya da temsilci süzgeci alınmaz. Ekip dışındaki tek kayıt 404.
|
|
43
|
+
Ekip süzgeci tek bağlı parametredir (`rep_org_id IN (SELECT value FROM json_each(?))`): kimlikleri SQL metnine
|
|
44
|
+
yazmayın. Ekip KESİLMEZ (üst sınır yok): kesilseydi sondaki kişilerin ziyaretleri yöneticiden sessizce gizlenirdi.
|
|
45
|
+
- **Ziyaretlerim yalnız kayıt sahibine açıktır** (`records: 'own'`); tüm ziyaretler yalnız okunur ve izinle kapılıdır.
|
|
46
|
+
- Zamanlanmış iş yoktur. "Sonraki adım tarihi gelince hatırlat" istenirse zamanlanmış işin e-postayı ancak manifest
|
|
47
|
+
`jobs.services` beyanı ve kurulumdaki yönetici onayıyla gönderebileceğini söyleyin.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# {{nameTr}}
|
|
2
|
+
|
|
3
|
+
Satış ekibinin müşteri görüşmelerini kaydettiği ve yöneticilerin ekiplerinin görüşmelerini izlediği bir uygulama.
|
|
4
|
+
Devoplus Portal'ın içinde, Portal menüsünden açılır. Bu uygulama Devoplus One'ın **müşteri ziyaret raporu**
|
|
5
|
+
şablonundan üretildi; ihtiyacınıza göre değiştirilecek bir başlangıç noktasıdır.
|
|
6
|
+
|
|
7
|
+
## Ne yapar
|
|
8
|
+
|
|
9
|
+
1. **Ziyaretlerim:** satış temsilcisi her görüşmeyi kaydeder: müşteri, ziyaret tarihi, görüşme türü (ziyaret, telefon,
|
|
10
|
+
çevrimiçi), görüşme notu, sonuç ve sonraki adım (tarihiyle). Ekranın üstünde bu haftaki görüşme sayısı, tarihi
|
|
11
|
+
geçmiş sonraki adımlar ve yaklaşan sonraki adımların listesi durur. Kendi görüşmelerini düzenler, sonraki adımı
|
|
12
|
+
tamamlandı olarak işaretler, listeyi Excel'e ya da CSV'ye aktarır.
|
|
13
|
+
2. **Müşteri seçimi:** kurumunuz ERP müşteri listesini bağladıysa müşteri listeden aranıp seçilir ve adı ERP'den
|
|
14
|
+
alınır. Liste bağlı değilse, uygulamanın izni yoksa ya da ERP'ye o an ulaşılamıyorsa ekran bunu söyler ve müşteri
|
|
15
|
+
adı elle yazılır; kayıt "Elle girildi" olarak işaretlenir. Uygulama ERP'ye **hiçbir şey yazmaz**, yalnız okur.
|
|
16
|
+
3. **Ekip ziyaretleri:** yönetici, organizasyon şemasında kendisine bağlı çalışanların görüşmelerini görür, arar,
|
|
17
|
+
süzer ve Excel'e aktarır. Ekip her açılışta organizasyon şemasından hesaplanır; şemada yöneticisi değişen çalışanın
|
|
18
|
+
görüşmeleri yeni yöneticisine görünür.
|
|
19
|
+
4. **Tüm ziyaretler:** bütün ziyaretleri görme izni olan kişi (ör. satış müdürü) bütün ekibin görüşmelerini görür.
|
|
20
|
+
5. **Devoplus asistanı:** "Bu hafta kaç müşteri görüşmesi yaptım?" ya da "Yaklaşan sonraki adımlarım neler?" gibi
|
|
21
|
+
sorular Devoplus'un yapay zekâ asistanına sorulduğunda cevap bu uygulamadan gelir.
|
|
22
|
+
|
|
23
|
+
## Kimler ne görür
|
|
24
|
+
|
|
25
|
+
| Kim | Ne yapabilir |
|
|
26
|
+
|---|---|
|
|
27
|
+
| Uygulamayı kullanma izni olan herkes ({{permissionCode}}) | Kendi görüşmelerini kaydeder ve görür; yöneticiyse ekibinin görüşmelerini görür |
|
|
28
|
+
| Bütün ziyaretleri görme izni ({{permissionPrefix}}Manage) | Bütün ekibin görüşmelerini görür ve Excel'e aktarır |
|
|
29
|
+
|
|
30
|
+
İzinleri portal yöneticisi **Yetki Yönetimi** ekranından verir. Kimin kimin yöneticisi olduğu Devoplus'un
|
|
31
|
+
organizasyon şemasından okunur; uygulamada ayrıca tanımlanmaz.
|
|
32
|
+
|
|
33
|
+
## ERP müşteri listesini bağlamak
|
|
34
|
+
|
|
35
|
+
Uygulama ilk üretildiğinde ERP'ye bağlı değildir ve müşteri elle yazılır. Bağlamak için kurumunuzun ERP'sinde müşteri
|
|
36
|
+
listesini döndüren bir sorgu gerekir. Yapay zekâ aracınıza "müşteri listesini ERP'den alalım" demeniz yeterlidir: araç
|
|
37
|
+
sorgunun bilgilerini sizden ya da yöneticinizden ister, uygulamanın ayarına yazar ve gereken izni yöneticiden ister.
|
|
38
|
+
Sorgu hazır değilse yöneticiden yeni bir sorgu yazılmasını isteyebilir.
|
|
39
|
+
|
|
40
|
+
## Neleri değiştirebilirsiniz
|
|
41
|
+
|
|
42
|
+
- "Sonuçlara *fiyat teklifi verildi* ekle" (ilk yayından önce netleştirin).
|
|
43
|
+
- "Görüşmeye katılan müşteri yetkilisinin adı da yazılsın" — yeni alan.
|
|
44
|
+
- "Yöneticinin yöneticisi de görsün" — ekip derinliği tek bir ayardır.
|
|
45
|
+
- "Sonraki adımın tarihi gelince temsilciye hatırlatılsın" — yeni bildirim.
|
|
46
|
+
|
|
47
|
+
Bilinmesi gerekenler: ziyaret tarihi bugünden ileri olamaz (gerçekleşen görüşme kaydedilir) ve bir yıldan eski olamaz.
|
|
48
|
+
Sonraki adım yazıldıysa tarihi de verilmelidir.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# {{nameTr}} — Devoplus yapay zekâsı için rehber
|
|
2
|
+
|
|
3
|
+
Bu dosyayı **Devoplus yapay zekâsı** okur (uygulama adresinde `/.well-known/one-agent.md`). Bir aracı eklerken,
|
|
4
|
+
değiştirirken ya da kaldırırken bu dosyayı AYNI değişiklikte güncelleyin; yayın kapısı dört başlığı ve her aracın adını
|
|
5
|
+
arar.
|
|
6
|
+
|
|
7
|
+
## Uygulama ne yapar
|
|
8
|
+
|
|
9
|
+
{{nameTr}}, satış ekibinin müşteri görüşmelerini kaydettiği bir rapor uygulamasıdır. Temsilci her görüşmeyi (ziyaret,
|
|
10
|
+
telefon ya da çevrimiçi) müşteri, tarih, görüşme notu, sonuç ve sonraki adımla kaydeder. Müşteri, kurum bağladıysa ERP
|
|
11
|
+
müşteri listesinden seçilir; bağlı değilse elle yazılır. Yönetici, organizasyon şemasında kendisine bağlı çalışanların
|
|
12
|
+
görüşmelerini görür. Kullanıcı "bu hafta kaç müşteri görüşmesi yaptım", "yaklaşan sonraki adımlarım neler", "en çok
|
|
13
|
+
hangi müşteriyle görüştük" gibi şeyler sorduğunda bu uygulamaya başvurun. Görüşme kaydeden ya da ERP'ye yazan araç
|
|
14
|
+
yoktur.
|
|
15
|
+
|
|
16
|
+
## Veri
|
|
17
|
+
|
|
18
|
+
- **Görüşme** (`visits`): müşteri kodu ve adı, müşteri kaynağı (`erp` ERP listesinden · `manual` elle girildi), satış
|
|
19
|
+
temsilcisi, ziyaret tarihi, görüşme türü (`visit` ziyaret · `phone` telefon · `online` çevrimiçi), görüşme notu,
|
|
20
|
+
sonuç (`interested` ilgileniyor · `order` sipariş/anlaşma · `follow_up` takip gerekiyor · `no_interest`
|
|
21
|
+
ilgilenmiyor), sonraki adım, sonraki adımın tarihi ve tamamlandı mı.
|
|
22
|
+
- Hafta pazartesi başlar, pazar biter (Türkiye saati).
|
|
23
|
+
|
|
24
|
+
## Araçlar
|
|
25
|
+
|
|
26
|
+
### `weekly-visit-summary` — bu haftaki ziyaret özeti (salt okunur)
|
|
27
|
+
|
|
28
|
+
- **Ne zaman:** kullanıcı bu haftaki müşteri görüşmelerini, sonuçlarını ya da yaklaşan sonraki adımlarını sorduğunda.
|
|
29
|
+
- **Girdi:** `scope` — `mine` (varsayılan: kullanıcının kendi görüşmeleri) ya da `all` (bütün ekip; yalnız bütün
|
|
30
|
+
ziyaretleri görme izni olan kişide çalışır, yoksa 403 döner — o zaman `mine` ile yeniden deneyin ve kullanıcıya
|
|
31
|
+
yalnız kendi görüşmelerini görebildiğini söyleyin).
|
|
32
|
+
- **Çıktı:** `{ "scope": "mine", "from": "2026-10-19", "to": "2026-10-25", "total": 7, "byType": { "visit": 4, "phone": 3 },
|
|
33
|
+
"byOutcome": { "interested": 3, "order": 2, "follow_up": 2 }, "topCustomers": [{ "customer_name", "visits" }],
|
|
34
|
+
"upcomingNextSteps": [{ "customer_name", "next_step", "next_step_date", "rep_name" }], "overdueNextSteps": 1 }` —
|
|
35
|
+
`topCustomers` en çok 5, `upcomingNextSteps` en çok 10 kayıt (en yakın tarih önce).
|
|
36
|
+
- **Dikkat:** "Ekibimin görüşmeleri" sorusunu bu araç cevaplamaz (ekip organizasyon şemasından ekranda hesaplanır);
|
|
37
|
+
kullanıcıyı uygulamanın Ekip ziyaretleri sekmesine yönlendirin.
|
|
38
|
+
|
|
39
|
+
## Örnek istekler
|
|
40
|
+
|
|
41
|
+
- "Bu hafta kaç müşteri görüşmesi yaptım?" → `weekly-visit-summary` → `total`, `byType`
|
|
42
|
+
- "Yaklaşan sonraki adımlarım neler?" → `weekly-visit-summary` → `upcomingNextSteps`
|
|
43
|
+
- "Bu hafta bütün ekip kaç sipariş aldı?" → `weekly-visit-summary` (`scope: "all"`) → `byOutcome.order`
|
|
44
|
+
- "Yarın ABC firmasını ziyaret ettiğimi kaydet." → araç yok; kullanıcıyı uygulamanın Ziyaretlerim sekmesine yönlendirin.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"entities": {
|
|
4
|
+
"visits": {
|
|
5
|
+
"title": { "tr": "Müşteri ziyaretleri", "en": "Customer visits" },
|
|
6
|
+
"fields": {
|
|
7
|
+
"customer_code": { "type": "text", "title": { "tr": "Müşteri kodu", "en": "Customer code" }, "maxLength": 40, "index": true },
|
|
8
|
+
"customer_name": { "type": "text", "title": { "tr": "Müşteri", "en": "Customer" }, "required": true, "maxLength": 160 },
|
|
9
|
+
"customer_source": {
|
|
10
|
+
"type": "choice",
|
|
11
|
+
"title": { "tr": "Müşteri kaynağı", "en": "Customer source" },
|
|
12
|
+
"required": true,
|
|
13
|
+
"default": "manual",
|
|
14
|
+
"choices": [
|
|
15
|
+
{ "value": "erp", "title": { "tr": "ERP müşteri listesi", "en": "ERP customer list" } },
|
|
16
|
+
{ "value": "manual", "title": { "tr": "Elle girildi", "en": "Entered manually" } }
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"rep_org_id": { "type": "text", "title": { "tr": "Satış temsilcisi (kimlik)", "en": "Sales rep (id)" }, "maxLength": 24, "index": true },
|
|
20
|
+
"rep_name": { "type": "text", "title": { "tr": "Satış temsilcisi", "en": "Sales rep" }, "maxLength": 120 },
|
|
21
|
+
"visit_date": { "type": "date", "title": { "tr": "Ziyaret tarihi", "en": "Visit date" }, "required": true, "index": true },
|
|
22
|
+
"visit_type": {
|
|
23
|
+
"type": "choice",
|
|
24
|
+
"title": { "tr": "Görüşme türü", "en": "Contact type" },
|
|
25
|
+
"required": true,
|
|
26
|
+
"default": "visit",
|
|
27
|
+
"index": true,
|
|
28
|
+
"choices": [
|
|
29
|
+
{ "value": "visit", "title": { "tr": "Ziyaret", "en": "Visit" } },
|
|
30
|
+
{ "value": "phone", "title": { "tr": "Telefon", "en": "Phone" } },
|
|
31
|
+
{ "value": "online", "title": { "tr": "Çevrimiçi", "en": "Online" } }
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
"notes": { "type": "longText", "title": { "tr": "Görüşme notu", "en": "Notes" }, "maxLength": 4000 },
|
|
35
|
+
"outcome": {
|
|
36
|
+
"type": "choice",
|
|
37
|
+
"title": { "tr": "Sonuç", "en": "Outcome" },
|
|
38
|
+
"required": true,
|
|
39
|
+
"index": true,
|
|
40
|
+
"choices": [
|
|
41
|
+
{ "value": "interested", "title": { "tr": "İlgileniyor", "en": "Interested" } },
|
|
42
|
+
{ "value": "order", "title": { "tr": "Sipariş / anlaşma", "en": "Order / deal" } },
|
|
43
|
+
{ "value": "follow_up", "title": { "tr": "Takip gerekiyor", "en": "Follow-up needed" } },
|
|
44
|
+
{ "value": "no_interest", "title": { "tr": "İlgilenmiyor", "en": "Not interested" } }
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
"next_step": { "type": "text", "title": { "tr": "Sonraki adım", "en": "Next step" }, "maxLength": 200 },
|
|
48
|
+
"next_step_date": { "type": "date", "title": { "tr": "Sonraki adım tarihi", "en": "Next step date" }, "index": true },
|
|
49
|
+
"next_step_done": { "type": "boolean", "title": { "tr": "Sonraki adım tamamlandı", "en": "Next step done" }, "required": true, "default": false }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Devoplus yapay zekâsının bu uygulamada çağırabileceği araçların İŞLEYİCİLERİ. Adı, açıklaması, girdi şeması,
|
|
2
|
+
// salt-okunurluğu ve izni `one.manifest.json` → `agent.tools`'tadır; rehber `AGENT_GUIDE.md`'dir. Yeni araç = üçü birlikte.
|
|
3
|
+
// Araçlar yalnız uygulamanın KENDİ veritabanını okur; ERP'ye gitmez. Ziyaret kaydeden ya da değiştiren araç bilinçli
|
|
4
|
+
// olarak yoktur. `all` kapsamı ekranla aynı kuralla bütün ziyaretleri görme iznine bağlıdır.
|
|
5
|
+
import { OneError, defineAgentTools, hasPermission } from '@devoplus/one-sdk'
|
|
6
|
+
import type { Env } from './app.ts'
|
|
7
|
+
import { overdueCount, upcomingSteps, weekSummary } from './queries.ts'
|
|
8
|
+
import { MANAGE_PERMISSION } from './visit.ts'
|
|
9
|
+
|
|
10
|
+
export const agentTools = defineAgentTools<Env>({
|
|
11
|
+
'weekly-visit-summary': {
|
|
12
|
+
handler: async ({ input, ctx, env }) => {
|
|
13
|
+
const scope = input.scope === 'all' ? 'all' : 'mine'
|
|
14
|
+
if (scope === 'all' && !hasPermission(ctx, MANAGE_PERMISSION))
|
|
15
|
+
throw new OneError(403, 'APP-E-403', 'Bütün ziyaretleri görmek için izniniz yok; yalnız kendi ziyaretlerinizi sorabilirsiniz.')
|
|
16
|
+
const owner = scope === 'all' ? null : (ctx.user?.sub ?? '')
|
|
17
|
+
const [week, upcoming, overdue] = await Promise.all([weekSummary(env, owner), upcomingSteps(env, owner, new Date(), 10), overdueCount(env, owner)])
|
|
18
|
+
return { scope, ...week, upcomingNextSteps: upcoming.map(({ id: _id, ...rest }) => rest), overdueNextSteps: overdue }
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
})
|