init
This commit is contained in:
commit
343169a973
8 changed files with 1783 additions and 0 deletions
302
skill/client.js
Normal file
302
skill/client.js
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Telegram Reader Client — удобный JS интерфейс к tg-proxy.wzray.com
|
||||
*
|
||||
* Usage:
|
||||
* node tg-client.js get-folders
|
||||
* node tg-client.js get-chats [--limit 50] [--offset 0] [--archived true|false] [--chat-type direct|bot|group|channel] [--folder-id N]
|
||||
* node tg-client.js get-messages <chat_id> [--limit 50] [--offset 0]
|
||||
* node tg-client.js delta <chat_id> <since> [--limit 50] [--offset 0]
|
||||
*
|
||||
* Examples:
|
||||
* node tg-client.js get-folders
|
||||
* node tg-client.js get-chats --limit 100 --chat-type group --folder-id 2
|
||||
* node tg-client.js get-messages 5880803391 --limit 10
|
||||
* node tg-client.js delta 5880803391 "2026-02-16T09:00:00Z"
|
||||
*/
|
||||
|
||||
const https = require('https');
|
||||
|
||||
const BASE_URL = 'tg-proxy.wzray.com';
|
||||
|
||||
function request(path, query = {}) {
|
||||
const queryString = new URLSearchParams(query).toString();
|
||||
const url = `${path}${queryString ? '?' + queryString : ''}`;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https.request(
|
||||
{
|
||||
hostname: BASE_URL,
|
||||
path: url,
|
||||
method: 'GET',
|
||||
headers: { 'User-Agent': 'openclaw-telegram-reader/1.0' }
|
||||
},
|
||||
(res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk) => data += chunk);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
resolve(JSON.parse(data));
|
||||
} catch (e) {
|
||||
reject(new Error(`JSON parse error: ${e.message}`));
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
req.on('error', reject);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
function getFlagValue(args, flag, fallback) {
|
||||
const idx = args.indexOf(flag);
|
||||
if (idx === -1 || idx + 1 >= args.length) return fallback;
|
||||
const parsed = parseInt(args[idx + 1], 10);
|
||||
return Number.isNaN(parsed) ? fallback : parsed;
|
||||
}
|
||||
|
||||
function getStringFlagValue(args, flag, fallback = undefined) {
|
||||
const idx = args.indexOf(flag);
|
||||
if (idx === -1 || idx + 1 >= args.length) return fallback;
|
||||
return args[idx + 1];
|
||||
}
|
||||
|
||||
function getBooleanFlagValue(args, flag, fallback = undefined) {
|
||||
const value = getStringFlagValue(args, flag, undefined);
|
||||
if (value === undefined) return fallback;
|
||||
if (value === 'true') return true;
|
||||
if (value === 'false') return false;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of chats
|
||||
*/
|
||||
async function getChats(limit = 50, offset = 0, options = {}) {
|
||||
const query = { limit, offset };
|
||||
if (options.archived !== undefined) query.archived = String(options.archived);
|
||||
if (options.chatType) query.chat_type = options.chatType;
|
||||
if (options.folderId !== undefined) query.folder_id = String(options.folderId);
|
||||
const result = await request('/chats', query);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of folders
|
||||
*/
|
||||
async function getFolders() {
|
||||
const result = await request('/folders');
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get messages in a specific chat
|
||||
*/
|
||||
async function getChatMessages(chatId, limit = 50, offset = 0) {
|
||||
const result = await request(`/chats/${chatId}/messages`, { limit, offset });
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get messages delta since timestamp for one chat
|
||||
*/
|
||||
async function getChatDelta(chatId, since, limit = 50, offset = 0) {
|
||||
const result = await request(`/chats/${chatId}/delta`, {
|
||||
since,
|
||||
limit,
|
||||
offset
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter chats by criteria
|
||||
*/
|
||||
function filterChats(chats, criteria) {
|
||||
const now = new Date();
|
||||
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
|
||||
return chats.filter(chat => {
|
||||
// Not muted
|
||||
if (criteria.notMuted && chat.is_muted) return false;
|
||||
|
||||
// Pinned
|
||||
if (criteria.pinned && !chat.is_pinned) return false;
|
||||
|
||||
// Active last week
|
||||
if (criteria.activeLastWeek) {
|
||||
if (!chat.last_message_date) return false;
|
||||
const lastMsg = new Date(chat.last_message_date);
|
||||
if (lastMsg < weekAgo) return false;
|
||||
}
|
||||
|
||||
// Inactive (no messages in last week)
|
||||
if (criteria.inactiveLastWeek) {
|
||||
if (!chat.last_message_date) return true; // no messages = inactive
|
||||
const lastMsg = new Date(chat.last_message_date);
|
||||
if (lastMsg >= weekAgo) return false; // has recent messages, skip
|
||||
}
|
||||
|
||||
// Type filter
|
||||
if (criteria.type) {
|
||||
if (criteria.type === 'direct' && chat.type !== 'private') return false;
|
||||
else if (criteria.type === 'bot' && chat.type !== 'bot') return false;
|
||||
else if (criteria.type === 'group' && !['group', 'supergroup', 'forum'].includes(chat.type)) return false;
|
||||
else if (criteria.type === 'channel' && chat.type !== 'channel') return false;
|
||||
}
|
||||
|
||||
// Archived filter
|
||||
if (criteria.archived !== undefined && chat.archived !== criteria.archived) return false;
|
||||
|
||||
// Folder filter
|
||||
if (criteria.folderId !== undefined && !(chat.folder_ids || []).includes(criteria.folderId)) return false;
|
||||
|
||||
// Has unread
|
||||
if (criteria.hasUnread && chat.unread_count === 0) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter messages by importance
|
||||
*/
|
||||
function filterImportantMessages(messages) {
|
||||
const keywords = ['важно', 'срочно', 'deadline', 'сегодня', '@wzray', 'визирей'];
|
||||
const lowerKeywords = keywords.map(k => k.toLowerCase());
|
||||
|
||||
return messages.filter(msg => {
|
||||
if (!msg.text) return false;
|
||||
|
||||
const text = msg.text.toLowerCase();
|
||||
|
||||
// Check for keywords
|
||||
if (lowerKeywords.some(kw => text.includes(kw))) return true;
|
||||
|
||||
// Mentions (basic check)
|
||||
if (text.includes('@')) return true;
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* CLI
|
||||
*/
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0) {
|
||||
console.log('Usage:');
|
||||
console.log(' node tg-client.js get-folders');
|
||||
console.log(' node tg-client.js get-chats [--limit N] [--offset N] [--archived true|false] [--chat-type direct|bot|group|channel] [--folder-id N]');
|
||||
console.log(' node tg-client.js get-messages <chat_id> [--limit N] [--offset N]');
|
||||
console.log(' node tg-client.js delta <chat_id> <since> [--limit N] [--offset N]');
|
||||
console.log(' node tg-client.js filter-chats [--pinned] [--not-muted] [--active-last-week]');
|
||||
console.log('');
|
||||
console.log('Examples:');
|
||||
console.log(' node tg-client.js get-folders');
|
||||
console.log(' node tg-client.js get-chats --limit 100 --chat-type group --folder-id 2');
|
||||
console.log(' node tg-client.js get-messages 5880803391 --limit 10');
|
||||
console.log(' node tg-client.js delta 5880803391 "2026-02-16T09:00:00Z"');
|
||||
console.log(' node tg-client.js filter-chats --pinned --not-muted --type=direct');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
const [command] = args;
|
||||
|
||||
switch (command) {
|
||||
case 'get-folders': {
|
||||
const result = await getFolders();
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'get-chats': {
|
||||
const limit = getFlagValue(args, '--limit', 50);
|
||||
const offset = getFlagValue(args, '--offset', 0);
|
||||
const archived = getBooleanFlagValue(args, '--archived', undefined);
|
||||
const chatType = getStringFlagValue(args, '--chat-type', undefined);
|
||||
const folderId = getFlagValue(args, '--folder-id', undefined);
|
||||
const result = await getChats(limit, offset, { archived, chatType, folderId });
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'get-messages': {
|
||||
const chatId = args[1];
|
||||
if (!chatId) throw new Error('chat_id is required');
|
||||
const limit = getFlagValue(args, '--limit', 50);
|
||||
const offset = getFlagValue(args, '--offset', 0);
|
||||
const result = await getChatMessages(chatId, limit, offset);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'delta': {
|
||||
const chatId = args[1];
|
||||
const since = args[2];
|
||||
if (!chatId || !since) throw new Error('chat_id and since are required');
|
||||
const limit = getFlagValue(args, '--limit', 50);
|
||||
const offset = getFlagValue(args, '--offset', 0);
|
||||
const result = await getChatDelta(chatId, since, limit, offset);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'filter-chats': {
|
||||
const limit = getFlagValue(args, '--limit', 100);
|
||||
const result = await getChats(limit);
|
||||
|
||||
if (!result || !result.items) {
|
||||
throw new Error('Invalid response from API');
|
||||
}
|
||||
|
||||
const criteria = {
|
||||
pinned: args.includes('--pinned'),
|
||||
notMuted: args.includes('--not-muted'),
|
||||
activeLastWeek: args.includes('--active-last-week'),
|
||||
inactiveLastWeek: args.includes('--inactive-last-week'),
|
||||
hasUnread: args.includes('--has-unread'),
|
||||
type: args.find(a => a.startsWith('--type='))?.split('=')[1],
|
||||
folderId: args.find(a => a.startsWith('--folder-id='))
|
||||
? parseInt(args.find(a => a.startsWith('--folder-id='))?.split('=')[1], 10)
|
||||
: undefined,
|
||||
archived: args.find(a => a.startsWith('--archived='))?.split('=')[1] === 'true'
|
||||
? true
|
||||
: args.find(a => a.startsWith('--archived='))?.split('=')[1] === 'false'
|
||||
? false
|
||||
: undefined
|
||||
};
|
||||
|
||||
const filtered = filterChats(result.items, criteria);
|
||||
console.log(JSON.stringify(filtered, null, 2));
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown command: ${command}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run CLI if called directly
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
// Export for use as a module
|
||||
module.exports = {
|
||||
getFolders,
|
||||
getChats,
|
||||
getChatMessages,
|
||||
getChatDelta,
|
||||
filterChats,
|
||||
filterImportantMessages
|
||||
};
|
||||
118
skill/skill.md
Normal file
118
skill/skill.md
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
---
|
||||
name: telegram-reader
|
||||
description: Read-only access to Telegram via tg-proxy API. Use when the user asks about recent messages, wants to search chats, or needs inbox summary.
|
||||
---
|
||||
|
||||
# Telegram Reader
|
||||
|
||||
Read-only interface to wzray's Telegram via `https://tg-proxy.wzray.com`.
|
||||
|
||||
## Endpoints
|
||||
|
||||
Base URL: `https://tg-proxy.wzray.com`
|
||||
|
||||
### 1. Get Chats
|
||||
```bash
|
||||
curl -s "https://tg-proxy.wzray.com/chats?limit=50&offset=0"
|
||||
```
|
||||
Returns: `{"items": [{id, title, type, chat_type, muted, archived, folder_id, folder_ids, pinned, ...}], "limit", "offset", "has_more", "remaining_count"}`
|
||||
|
||||
Supported query params:
|
||||
- `limit`
|
||||
- `offset`
|
||||
- `archived=true|false`
|
||||
- `chat_type=direct|bot|group|channel`
|
||||
- `folder_id` — filter chats by Telegram folder/dialog filter id (get list from `/folders`)
|
||||
|
||||
### 2. Get Folders
|
||||
```bash
|
||||
curl -s "https://tg-proxy.wzray.com/folders"
|
||||
```
|
||||
Returns: `[{id, title, type, icon_emoji, pinned_chat_ids, include_chat_ids, exclude_chat_ids, contacts, non_contacts, groups, broadcasts, bots, exclude_muted, exclude_read, exclude_archived, has_my_invites}]`
|
||||
|
||||
### 3. Get Messages in Chat
|
||||
```bash
|
||||
curl -s "https://tg-proxy.wzray.com/chats/{chat_id}/messages?limit=50&offset=0"
|
||||
```
|
||||
Returns: `{"chat": {...}, "items": [{id, date, text, from_user, chat_id, from_me, is_outgoing, reply_to_message_id, quoted_text, reply_snippet, edited_at, is_read, attachments}], "limit", "offset", "has_more"}`
|
||||
|
||||
### 4. Get Messages Delta
|
||||
```bash
|
||||
curl -s "https://tg-proxy.wzray.com/chats/{chat_id}/delta?since=2026-02-16T06:00:00Z&limit=50&offset=0"
|
||||
```
|
||||
Returns: `{"chat": {...}, "items": [...], "limit", "offset", "has_more", "remaining_count"}`
|
||||
|
||||
Notes:
|
||||
- `since` is required for `delta`
|
||||
- `delta` returns messages in chronological order
|
||||
- `offset` is applied inside the filtered `since` window
|
||||
- `remaining_count` is present where the server can compute it cheaply and accurately
|
||||
- `null` fields are omitted from JSON output
|
||||
- responses are pretty-printed JSON
|
||||
|
||||
## State
|
||||
|
||||
State stored in: `runtime/telegram-reader/state.json` (gitignored, persists locally)
|
||||
|
||||
```json
|
||||
{
|
||||
"last_check": "2026-02-16T10:30:00Z",
|
||||
"watched_chats": [-1003680985286, ...],
|
||||
"watched_names": {"-1003680985286": "is-tech-y28", ...},
|
||||
"last_message_ids": {}
|
||||
}
|
||||
```
|
||||
|
||||
## JS Client
|
||||
|
||||
`skills/scripts/telegram-reader/tg-client.js` — удобный CLI для API:
|
||||
|
||||
```bash
|
||||
# List folders
|
||||
node skills/scripts/telegram-reader/tg-client.js get-folders
|
||||
|
||||
# List chats
|
||||
node skills/scripts/telegram-reader/tg-client.js get-chats --limit 50
|
||||
|
||||
# Only archived groups
|
||||
node skills/scripts/telegram-reader/tg-client.js get-chats --archived true --chat-type group
|
||||
|
||||
# Filter chats by folder
|
||||
node skills/scripts/telegram-reader/tg-client.js get-chats --folder-id 2
|
||||
|
||||
# Filter chats (client-side)
|
||||
node skills/scripts/telegram-reader/tg-client.js filter-chats --active-last-week --not-muted --type=direct
|
||||
|
||||
# Get delta
|
||||
node skills/scripts/telegram-reader/tg-client.js delta 5880803391 "2026-02-16T00:00:00Z"
|
||||
```
|
||||
|
||||
## Behavior
|
||||
|
||||
### On Heartbeat (hourly)
|
||||
1. Load state
|
||||
2. For each watched chat, get delta since `last_check`
|
||||
3. Filter important messages:
|
||||
- Mentions (@wzray)
|
||||
- Keywords: "важно", "срочно", "deadline", "сегодня"
|
||||
- Unread DMs
|
||||
4. Summarize and notify if significant
|
||||
5. Update `last_check` to now
|
||||
|
||||
### When User Asks
|
||||
- "Что нового?" → run delta since last_check, summarize
|
||||
- "Что писал [кто-то]?" → search in recent messages
|
||||
- "Покажи чаты" → list chats from API
|
||||
|
||||
## Priority Filters
|
||||
|
||||
For heartbeat summaries, prioritize:
|
||||
1. DMs with unread/unanswered messages
|
||||
2. Messages mentioning user
|
||||
3. Messages with urgency keywords
|
||||
4. Pinned/important chats
|
||||
|
||||
Ignore:
|
||||
- Channels (unless user explicitly watches them)
|
||||
- Muted chats
|
||||
- Large groups with high traffic (unless mentioned)
|
||||
Loading…
Add table
Add a link
Reference in a new issue