@gobing-ai/knowledge-kit 0.0.11 → 0.0.13
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/index.js +451 -152
- package/package.json +1 -1
- package/plugins/generations/content-gen/dist/index.js +22167 -0
- package/plugins/generations/content-gen/plugin.json +1 -1
- package/plugins/generations/content-gen/src/storm.ts +99 -50
- package/plugins/generations/core-facts-gen/dist/index.js +22048 -0
- package/plugins/generations/core-facts-gen/package.json +17 -0
- package/plugins/generations/core-facts-gen/plugin.json +7 -0
- package/plugins/generations/core-facts-gen/src/index.ts +116 -0
- package/plugins/generations/core-facts-gen/tsconfig.json +4 -0
- package/plugins/generations/daily-article-gen/dist/index.js +22022 -0
- package/plugins/generations/daily-article-gen/package.json +17 -0
- package/plugins/generations/daily-article-gen/plugin.json +7 -0
- package/plugins/generations/daily-article-gen/src/index.ts +91 -0
- package/plugins/generations/daily-article-gen/tsconfig.json +4 -0
- package/plugins/generations/dailynews-gen/dist/index.js +22324 -0
- package/plugins/generations/dailynews-gen/plugin.json +1 -1
- package/plugins/generations/dailynews-gen/src/index.ts +11 -0
- package/plugins/generations/dailynews-gen/src/script-builder.ts +1 -1
- package/plugins/generations/episode-plan-gen/dist/index.js +22479 -0
- package/plugins/generations/episode-plan-gen/package.json +17 -0
- package/plugins/generations/episode-plan-gen/plugin.json +7 -0
- package/plugins/generations/episode-plan-gen/src/index.ts +726 -0
- package/plugins/generations/episode-plan-gen/tsconfig.json +4 -0
- package/plugins/generations/image-gen/config.example.yaml +75 -0
- package/plugins/generations/image-gen/dist/index.js +22619 -0
- package/plugins/generations/image-gen/package.json +17 -0
- package/plugins/generations/image-gen/plugin.json +7 -0
- package/plugins/generations/image-gen/presets/formats/cover.yaml +57 -0
- package/plugins/generations/image-gen/presets/formats/free.yaml +46 -0
- package/plugins/generations/image-gen/presets/formats/illustration.yaml +48 -0
- package/plugins/generations/image-gen/presets/styles/clean-webapp-ui.yaml +28 -0
- package/plugins/generations/image-gen/presets/styles/cute.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/editorial.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/fresh.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/minimalist.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/photorealistic.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/sketch.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/technical-diagram.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/vibrant.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/warm.yaml +3 -0
- package/plugins/generations/image-gen/src/bytes.ts +19 -0
- package/plugins/generations/image-gen/src/index.ts +319 -0
- package/plugins/generations/image-gen/src/job.ts +143 -0
- package/plugins/generations/image-gen/src/paths.ts +31 -0
- package/plugins/generations/image-gen/src/presets.ts +344 -0
- package/plugins/generations/image-gen/src/providers/seedream.ts +128 -0
- package/plugins/generations/image-gen/src/providers/types.ts +285 -0
- package/plugins/generations/image-gen/tsconfig.json +8 -0
- package/plugins/generations/voice-gen/dist/index.js +23027 -0
- package/plugins/generations/voice-gen/plugin.json +1 -1
- package/plugins/generations/voice-gen/src/index.ts +102 -11
- package/plugins/generations/voice-gen/src/qc.ts +154 -9
- package/plugins/ingestions/aihot-ingest/dist/index.js +22358 -0
- package/plugins/ingestions/aihot-ingest/plugin.json +2 -2
- package/plugins/ingestions/aihot-ingest/src/index.ts +72 -13
- package/plugins/ingestions/aihot-ingest/src/mapper.ts +1 -0
- package/plugins/ingestions/aihot-ingest/src/rss.ts +151 -0
- package/plugins/ingestions/horizon-ingest/dist/index.js +22105 -0
- package/plugins/ingestions/horizon-ingest/package.json +17 -0
- package/plugins/ingestions/horizon-ingest/plugin.json +7 -0
- package/plugins/ingestions/horizon-ingest/src/index.ts +205 -0
- package/plugins/ingestions/horizon-ingest/tsconfig.json +4 -0
- package/plugins/ingestions/karakeep-local/dist/index.js +24204 -0
- package/plugins/ingestions/karakeep-local/plugin.json +1 -1
- package/plugins/ingestions/last30days-ingest/dist/index.js +22050 -0
- package/plugins/ingestions/last30days-ingest/package.json +17 -0
- package/plugins/ingestions/last30days-ingest/plugin.json +7 -0
- package/plugins/ingestions/last30days-ingest/src/index.ts +148 -0
- package/plugins/ingestions/last30days-ingest/tsconfig.json +4 -0
- package/plugins/ingestions/web-search/dist/index.js +24379 -0
- package/plugins/ingestions/web-search/plugin.json +1 -1
- package/plugins/kk/commands/image-generate.md +15 -0
- package/plugins/kk/config.example.yaml +80 -0
- package/plugins/kk/skills/image-authoring/SKILL.md +250 -0
- package/plugins/kk/skills/image-authoring/references/illustration-positions.md +87 -0
- package/plugins/kk/skills/image-authoring/references/migrating-from-wt.md +31 -0
- package/plugins/kk/skills/image-authoring/references/providers.md +52 -0
- package/plugins/kk/skills/image-authoring/references/style-extraction.md +134 -0
- package/plugins/kk/skills/taste-unslop/SKILL.md +12 -6
- package/plugins/kk/skills/taste-unslop/references/pattern-guide.md +128 -48
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +432 -19
- package/plugins/publishings/emdash-pub/dist/index.js +22243 -0
- package/plugins/publishings/emdash-pub/plugin.json +1 -1
- package/plugins/publishings/podcast-pub/dist/index.js +22596 -0
- package/plugins/publishings/podcast-pub/package.json +17 -0
- package/plugins/publishings/podcast-pub/plugin.json +13 -0
- package/plugins/publishings/podcast-pub/src/index.ts +538 -0
- package/plugins/publishings/podcast-pub/src/map.ts +165 -0
- package/plugins/publishings/podcast-pub/src/microfeed-client.ts +196 -0
- package/plugins/publishings/podcast-pub/src/show-notes.ts +132 -0
- package/plugins/publishings/podcast-pub/tsconfig.json +4 -0
- package/plugins/publishings/qiita-pub/dist/index.js +22081 -0
- package/plugins/publishings/qiita-pub/plugin.json +1 -1
- package/plugins/publishings/surfdash-pub/dist/index.js +22233 -0
- package/plugins/publishings/surfdash-pub/plugin.json +1 -1
- package/plugins/publishings/surfdash-pub/src/index.ts +328 -62
- package/plugins/publishings/zenn-pub/dist/index.js +22122 -0
- package/plugins/publishings/zenn-pub/plugin.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aihot-ingest",
|
|
3
3
|
"kind": "ingestion",
|
|
4
|
-
"entry": "./
|
|
4
|
+
"entry": "./dist/index.js",
|
|
5
5
|
"version": "1.0.0",
|
|
6
|
-
"description": "Ingests
|
|
6
|
+
"description": "Ingests fresh AI news from an RSS feed (default TechCrunch AI) into Doc[]; legacy aihot.virxact.com API behind AIHOT_SOURCE=api"
|
|
7
7
|
}
|
|
@@ -6,9 +6,11 @@ import { echoError } from '@gobing-ai/ts-utils';
|
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
import { type AihotDeps, createAihotClient } from './client';
|
|
8
8
|
import { mapAihotItemsToDocs } from './mapper';
|
|
9
|
+
import { AIHOT_FEED_URL_DEFAULT, fetchRssDocs, type RssFetchLike } from './rss';
|
|
9
10
|
|
|
10
11
|
export * from './client';
|
|
11
12
|
export * from './mapper';
|
|
13
|
+
export * from './rss';
|
|
12
14
|
|
|
13
15
|
export interface IngestionOptions {
|
|
14
16
|
out: string;
|
|
@@ -38,8 +40,26 @@ function parseLimit(value: string): number {
|
|
|
38
40
|
export async function processIngestionIO(
|
|
39
41
|
options: IngestionOptions,
|
|
40
42
|
depsOverride?: Partial<AihotDeps>,
|
|
43
|
+
rssFetchLike: RssFetchLike = fetch,
|
|
41
44
|
): Promise<IngestionResult> {
|
|
42
45
|
const fs = createNodeFileSystem();
|
|
46
|
+
|
|
47
|
+
// RSS is the default source (2026-09-08 dogfood: the aihot.virxact.com API served a
|
|
48
|
+
// 2020–2025 archive — every item failed the 7-day freshness gate; see rss.ts). The API
|
|
49
|
+
// path stays reachable with AIHOT_SOURCE=api for when the CMS carries live curated items.
|
|
50
|
+
const source = process.env.AIHOT_SOURCE?.trim() || 'rss';
|
|
51
|
+
if (source === 'api') {
|
|
52
|
+
return processApiSource(options, fs, depsOverride);
|
|
53
|
+
}
|
|
54
|
+
return processRssSource(options, fs, rssFetchLike);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Legacy aihot.virxact.com API source (cursor/stateFile contract preserved). */
|
|
58
|
+
async function processApiSource(
|
|
59
|
+
options: IngestionOptions,
|
|
60
|
+
fs: ReturnType<typeof createNodeFileSystem>,
|
|
61
|
+
depsOverride?: Partial<AihotDeps>,
|
|
62
|
+
): Promise<IngestionResult> {
|
|
43
63
|
const client = createAihotClient(depsOverride);
|
|
44
64
|
|
|
45
65
|
let effectiveCursor = options.cursor?.trim();
|
|
@@ -68,30 +88,69 @@ export async function processIngestionIO(
|
|
|
68
88
|
}
|
|
69
89
|
|
|
70
90
|
const docs = DocListSchema.parse(mapAihotItemsToDocs(items));
|
|
71
|
-
|
|
72
|
-
const outDir = dirname(options.out);
|
|
73
|
-
if (outDir && outDir !== '.') {
|
|
74
|
-
await fs.ensureDir(outDir);
|
|
75
|
-
}
|
|
76
|
-
await atomicWriteJson(options.out, docs, fs);
|
|
91
|
+
await writeOutput(fs, options, docs);
|
|
77
92
|
|
|
78
93
|
if (options.stateFile) {
|
|
79
|
-
|
|
80
|
-
if (stateDir && stateDir !== '.') {
|
|
81
|
-
await fs.ensureDir(stateDir);
|
|
82
|
-
}
|
|
83
|
-
const statePayload = {
|
|
94
|
+
await writeState(fs, options.stateFile, {
|
|
84
95
|
cursor,
|
|
85
96
|
asOf,
|
|
86
97
|
lastSyncAt: new Date().toISOString(),
|
|
87
98
|
itemCount: docs.length,
|
|
88
|
-
};
|
|
89
|
-
await atomicWriteJson(options.stateFile, statePayload, fs);
|
|
99
|
+
});
|
|
90
100
|
}
|
|
91
101
|
|
|
92
102
|
return { docs, cursor, asOf };
|
|
93
103
|
}
|
|
94
104
|
|
|
105
|
+
/** RSS feed source (default): stateless fetch, no cursor (feed order is newest-first). */
|
|
106
|
+
async function processRssSource(
|
|
107
|
+
options: IngestionOptions,
|
|
108
|
+
fs: ReturnType<typeof createNodeFileSystem>,
|
|
109
|
+
rssFetchLike: RssFetchLike,
|
|
110
|
+
): Promise<IngestionResult> {
|
|
111
|
+
const feedUrl = process.env.AIHOT_FEED_URL?.trim() || AIHOT_FEED_URL_DEFAULT;
|
|
112
|
+
const limit = options.limit ?? 10;
|
|
113
|
+
const { docs, asOf } = await fetchRssDocs(feedUrl, limit, rssFetchLike);
|
|
114
|
+
const validated = DocListSchema.parse(docs);
|
|
115
|
+
await writeOutput(fs, options, validated);
|
|
116
|
+
|
|
117
|
+
if (options.stateFile) {
|
|
118
|
+
await writeState(fs, options.stateFile, {
|
|
119
|
+
source: 'rss',
|
|
120
|
+
feedUrl,
|
|
121
|
+
asOf,
|
|
122
|
+
lastSyncAt: new Date().toISOString(),
|
|
123
|
+
itemCount: validated.length,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { docs: validated, cursor: '', asOf };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function writeOutput(
|
|
131
|
+
fs: ReturnType<typeof createNodeFileSystem>,
|
|
132
|
+
options: IngestionOptions,
|
|
133
|
+
docs: Doc[],
|
|
134
|
+
): Promise<void> {
|
|
135
|
+
const outDir = dirname(options.out);
|
|
136
|
+
if (outDir && outDir !== '.') {
|
|
137
|
+
await fs.ensureDir(outDir);
|
|
138
|
+
}
|
|
139
|
+
await atomicWriteJson(options.out, docs, fs);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function writeState(
|
|
143
|
+
fs: ReturnType<typeof createNodeFileSystem>,
|
|
144
|
+
stateFile: string,
|
|
145
|
+
payload: Record<string, unknown>,
|
|
146
|
+
): Promise<void> {
|
|
147
|
+
const stateDir = dirname(stateFile);
|
|
148
|
+
if (stateDir && stateDir !== '.') {
|
|
149
|
+
await fs.ensureDir(stateDir);
|
|
150
|
+
}
|
|
151
|
+
await atomicWriteJson(stateFile, payload, fs);
|
|
152
|
+
}
|
|
153
|
+
|
|
95
154
|
export async function main(depsOverride?: Partial<AihotDeps>): Promise<number> {
|
|
96
155
|
let values: { in?: string; out?: string; limit?: string; cursor?: string; 'state-file'?: string };
|
|
97
156
|
try {
|
|
@@ -16,6 +16,7 @@ export function mapAihotItemToDoc(item: AihotItem): Doc {
|
|
|
16
16
|
const body = `${item.summary?.trim() || item.title.trim()}\n`;
|
|
17
17
|
|
|
18
18
|
const metadata: Record<string, unknown> = {
|
|
19
|
+
source: 'aihot',
|
|
19
20
|
sourceName: item.source.name,
|
|
20
21
|
publishedAt: item.publishedAt,
|
|
21
22
|
discoveredAt: item.discoveredAt,
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import type { Doc } from '@gobing-ai/kk-core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* RSS/Atom feed fetch + item → Doc mapping for the aihot-ingest RSS source.
|
|
6
|
+
*
|
|
7
|
+
* Why RSS (2026-09-08, dogfood): aihot.virxact.com's API served a 2020–2025 blog
|
|
8
|
+
* archive — all 10 items were dropped every run by the workflow's 7-day freshness
|
|
9
|
+
* gate, so the "curated" aihot source contributed zero usable items while carrying
|
|
10
|
+
* the highest selection tier (SOURCE_BASE aihot=30, importance 4). The RSS source
|
|
11
|
+
* restores aihot as a genuinely fresh feed; the API path stays selectable via
|
|
12
|
+
* AIHOT_SOURCE=api for when the CMS has live curated content.
|
|
13
|
+
*
|
|
14
|
+
* Keeps metadata.source='aihot' so planEpisode's aihot tier (and importance base 4)
|
|
15
|
+
* still applies to feed items.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Default fresh AI-news feed (RSS 2.0, high volume, reachable without auth). */
|
|
19
|
+
export const AIHOT_FEED_URL_DEFAULT = 'https://techcrunch.com/category/artificial-intelligence/feed/';
|
|
20
|
+
|
|
21
|
+
export type RssFetchLike = (url: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
22
|
+
|
|
23
|
+
export interface RssEntry {
|
|
24
|
+
title: string;
|
|
25
|
+
link: string;
|
|
26
|
+
publishedAt: string | null; // ISO-8601 or null
|
|
27
|
+
summary: string;
|
|
28
|
+
category: string | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Strip XML/HTML tags and normalize common entities to plain text. */
|
|
32
|
+
function stripMarkup(raw: string): string {
|
|
33
|
+
return raw
|
|
34
|
+
.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1')
|
|
35
|
+
.replace(/<[^>]+>/g, ' ')
|
|
36
|
+
.replace(/ /g, ' ')
|
|
37
|
+
.replace(/&/g, '&')
|
|
38
|
+
.replace(/</g, '<')
|
|
39
|
+
.replace(/>/g, '>')
|
|
40
|
+
.replace(/"/g, '"')
|
|
41
|
+
.replace(/'|'/g, "'")
|
|
42
|
+
.replace(/\s+/g, ' ')
|
|
43
|
+
.trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** RSS <pubDate> (RFC 822) or Atom <published>/<updated> (ISO-8601) → ISO-8601. */
|
|
47
|
+
function toIsoDate(raw: string | undefined): string | null {
|
|
48
|
+
if (!raw?.trim()) return null;
|
|
49
|
+
const t = Date.parse(raw.trim().replace('UT', 'GMT'));
|
|
50
|
+
return Number.isFinite(t) ? new Date(t).toISOString() : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Extract a single sub-element's text from an element body. */
|
|
54
|
+
function subElement(body: string, tag: string): string | undefined {
|
|
55
|
+
const re = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`, 'i');
|
|
56
|
+
const m = body.match(re);
|
|
57
|
+
return m?.[1];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Extract a link: RSS <link>text</link> or Atom <link href="..."/> (first match wins). */
|
|
61
|
+
function subLink(body: string): string | undefined {
|
|
62
|
+
const text = subElement(body, 'link');
|
|
63
|
+
if (text?.trim()) return text.trim();
|
|
64
|
+
const m = body.match(/<link[^>]*\bhref\s*=\s*"([^"]+)"/i);
|
|
65
|
+
return m?.[1];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Block-split RSS <item>… or Atom <entry>… elements and map each to an RssEntry. */
|
|
69
|
+
export function parseFeed(xml: string): RssEntry[] {
|
|
70
|
+
const blocks: string[] = [];
|
|
71
|
+
const itemRe = /<item[\s>][\s\S]*?<\/item>/gi;
|
|
72
|
+
const entryRe = /<entry[\s>][\s\S]*?<\/entry>/gi;
|
|
73
|
+
let m = itemRe.exec(xml);
|
|
74
|
+
while (m !== null) {
|
|
75
|
+
blocks.push(m[0]);
|
|
76
|
+
m = itemRe.exec(xml);
|
|
77
|
+
}
|
|
78
|
+
if (blocks.length === 0) {
|
|
79
|
+
m = entryRe.exec(xml);
|
|
80
|
+
while (m !== null) {
|
|
81
|
+
blocks.push(m[0]);
|
|
82
|
+
m = entryRe.exec(xml);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return blocks.map((b) => {
|
|
86
|
+
const title = stripMarkup(subElement(b, 'title') ?? '');
|
|
87
|
+
const link = subLink(b) ?? '';
|
|
88
|
+
const category = stripMarkup(subElement(b, 'category') ?? '') || null;
|
|
89
|
+
const pubRaw = subElement(b, 'pubDate') ?? subElement(b, 'published') ?? subElement(b, 'updated');
|
|
90
|
+
const summary = stripMarkup(
|
|
91
|
+
subElement(b, 'content:encoded') ?? subElement(b, 'description') ?? subElement(b, 'summary') ?? title,
|
|
92
|
+
);
|
|
93
|
+
return { title, link, publishedAt: toIsoDate(pubRaw), summary, category };
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function computeRssDocId(seed: string): string {
|
|
98
|
+
return createHash('sha256').update(seed).digest('hex').slice(0, 16);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Map a feed entry to the kk Doc shape; source:'aihot' keeps the aihot plan tier. */
|
|
102
|
+
export function mapRssEntryToDoc(entry: RssEntry, feedUrl: string): Doc {
|
|
103
|
+
const id = entry.link ? computeRssDocId(entry.link) : computeRssDocId(`${entry.title}${entry.summary}`);
|
|
104
|
+
let sourceName = 'aihot-feed';
|
|
105
|
+
try {
|
|
106
|
+
sourceName = new URL(feedUrl).hostname;
|
|
107
|
+
} catch {
|
|
108
|
+
// keep 'aihot-feed' fallback
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
id,
|
|
112
|
+
title: entry.title || 'Untitled AI News',
|
|
113
|
+
sourceUri: entry.link || feedUrl,
|
|
114
|
+
body: `${entry.title}\n\n${entry.summary}\n`,
|
|
115
|
+
mediaType: 'text/markdown',
|
|
116
|
+
metadata: {
|
|
117
|
+
source: 'aihot',
|
|
118
|
+
sourceName,
|
|
119
|
+
publishedAt: entry.publishedAt ?? undefined,
|
|
120
|
+
discoveredAt: new Date().toISOString(),
|
|
121
|
+
originalUrl: entry.link || undefined,
|
|
122
|
+
category: entry.category ?? undefined,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function fetchRssDocs(
|
|
128
|
+
feedUrl: string,
|
|
129
|
+
limit: number,
|
|
130
|
+
fetchLike: RssFetchLike = fetch,
|
|
131
|
+
): Promise<{ docs: Doc[]; asOf: string }> {
|
|
132
|
+
const res = await fetchLike(feedUrl, {
|
|
133
|
+
headers: { 'user-agent': 'aihot-ingest/1.0.0 (RSS source)' },
|
|
134
|
+
});
|
|
135
|
+
if (!res.ok) {
|
|
136
|
+
throw new Error(`aihot-ingest RSS fetch failed: HTTP ${res.status} from ${feedUrl}`);
|
|
137
|
+
}
|
|
138
|
+
const xml = await res.text();
|
|
139
|
+
const entries = parseFeed(xml);
|
|
140
|
+
// Feed order is newest-first for RSS; atom may vary — prefer publication order.
|
|
141
|
+
const seen = new Set<string>();
|
|
142
|
+
const docs: Doc[] = [];
|
|
143
|
+
for (const entry of entries) {
|
|
144
|
+
if (docs.length >= limit) break;
|
|
145
|
+
const key = entry.link || entry.title;
|
|
146
|
+
if (seen.has(key)) continue;
|
|
147
|
+
seen.add(key);
|
|
148
|
+
docs.push(mapRssEntryToDoc(entry, feedUrl));
|
|
149
|
+
}
|
|
150
|
+
return { docs, asOf: new Date().toISOString() };
|
|
151
|
+
}
|